Add more user management features

This commit is contained in:
Michael Zanetti 2020-01-30 11:44:49 +01:00
parent 7ead4ba91e
commit ec0aa802c5
14 changed files with 728 additions and 64 deletions

View File

@ -70,6 +70,7 @@
#include "networkmanagerhandler.h" #include "networkmanagerhandler.h"
#include "tagshandler.h" #include "tagshandler.h"
#include "systemhandler.h" #include "systemhandler.h"
#include "usershandler.h"
#include <QJsonDocument> #include <QJsonDocument>
#include <QStringList> #include <QStringList>
@ -194,13 +195,13 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Return a list of TokenInfo objects of all the tokens for the current user."; description = "Return a list of TokenInfo objects of all the tokens for the current user.";
returns.insert("tokenInfoList", QVariantList() << objectRef("TokenInfo")); returns.insert("tokenInfoList", QVariantList() << objectRef("TokenInfo"));
registerMethod("Tokens", description, params, returns); registerMethod("Tokens", description, params, returns, "Use Users.GetTokens instead.");
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Revoke access for a given token."; description = "Revoke access for a given token.";
params.insert("tokenId", enumValueName(Uuid)); params.insert("tokenId", enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("RemoveToken", description, params, returns); registerMethod("RemoveToken", description, params, returns, "Use Users.RemoveToken instead.");
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Sets up the cloud connection by deploying a certificate and its configuration."; description = "Sets up the cloud connection by deploying a certificate and its configuration.";
@ -370,8 +371,8 @@ JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap &params) const
Q_UNUSED(params) Q_UNUSED(params)
QByteArray token = property("token").toByteArray(); QByteArray token = property("token").toByteArray();
QString username = NymeaCore::instance()->userManager()->userForToken(token); TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token);
QList<TokenInfo> tokens = NymeaCore::instance()->userManager()->tokens(username); QList<TokenInfo> tokens = NymeaCore::instance()->userManager()->tokens(tokenInfo.username());
QVariantList retList; QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) { foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo); retList << pack(tokenInfo);
@ -569,6 +570,7 @@ void JsonRPCServerImplementation::setup()
registerHandler(new NetworkManagerHandler(this)); registerHandler(new NetworkManagerHandler(this));
registerHandler(new TagsHandler(this)); registerHandler(new TagsHandler(this));
registerHandler(new SystemHandler(NymeaCore::instance()->platform(), this)); registerHandler(new SystemHandler(NymeaCore::instance()->platform(), this));
registerHandler(new UsersHandler(NymeaCore::instance()->userManager(), this));
connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServerImplementation::pairingFinished); connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServerImplementation::pairingFinished);
connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectionStateChanged, this, &JsonRPCServerImplementation::onCloudConnectionStateChanged); connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectionStateChanged, this, &JsonRPCServerImplementation::onCloudConnectionStateChanged);
@ -634,19 +636,19 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
// check if authentication is required for this transport // check if authentication is required for this transport
if (m_interfaces.value(interface)) { if (m_interfaces.value(interface)) {
QByteArray token = message.value("token").toByteArray(); QByteArray token = message.value("token").toByteArray();
QStringList authExemptMethodsNoUser = {"Introspect", "Hello", "CreateUser", "RequestPushButtonAuth"}; QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.CreateUser"};
QStringList authExemptMethodsWithUser = {"Introspect", "Hello", "Authenticate", "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 special method for authentication itself // if there is no user in the system yet, let's fail unless this is special method for authentication itself
if (NymeaCore::instance()->userManager()->initRequired()) { if (NymeaCore::instance()->userManager()->initRequired()) {
if (!(targetNamespace == "JSONRPC" && authExemptMethodsNoUser.contains(method)) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { if (!authExemptMethodsNoUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call CreateUser first."); sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call Users.CreateUser first.");
qCWarning(dcJsonRpc()) << "Initial setup required but client does not call the setup. Dropping connection."; qCWarning(dcJsonRpc()) << "Initial setup required but client does not call the setup. Dropping connection.";
interface->terminateClientConnection(clientId); interface->terminateClientConnection(clientId);
return; return;
} }
} else { } else {
// ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect Hello call // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect Hello call
if (!(targetNamespace == "JSONRPC" && authExemptMethodsWithUser.contains(method)) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { if (!authExemptMethodsWithUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token.");
qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection."; qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection.";
interface->terminateClientConnection(clientId); interface->terminateClientConnection(clientId);

View File

@ -0,0 +1,216 @@
#include "usershandler.h"
#include "usermanager/usermanager.h"
#include "usermanager/userinfo.h"
#include "loggingcategories.h"
namespace nymeaserver {
UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
JsonHandler(parent),
m_userManager(userManager)
{
registerObject<UserInfo>();
registerObject<TokenInfo, TokenInfoList>();
QVariantMap params, returns;
QString description;
params.clear(); returns.clear();
description = "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user.";
params.insert("username", enumValueName(String));
params.insert("password", enumValueName(String));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("CreateUser", description, params, returns);
params.clear(); returns.clear();
description = "Change the password for the currently logged in user.";
params.insert("newPassword", enumValueName(String));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("ChangePassword", description, params, returns);
params.clear(); returns.clear();
description = "Get info about the current token (the currently logged in user).";
returns.insert("o:userInfo", objectRef<UserInfo>());
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("GetUserInfo", description, params, returns);
params.clear(); returns.clear();
description = "Get all the tokens for the current user.";
returns.insert("o:tokenInfoList", objectRef<TokenInfoList>());
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("GetTokens", description, params, returns);
params.clear(); returns.clear();
description = "Revoke access for a given token.";
params.insert("tokenId", enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("RemoveToken", description, params, returns);
}
QString UsersHandler::name() const
{
return "Users";
}
JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
{
QString username = params.value("username").toString();
QString password = params.value("password").toString();
UserManager::UserError status = m_userManager->createUser(username, password);
QVariantMap returns;
returns.insert("error", enumValueName<UserManager::UserError>(status));
return createReply(returns);
}
JsonReply *UsersHandler::ChangePassword(const QVariantMap &params)
{
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
QString newPassword = params.value("newPassword").toString();
QString username = m_userManager->userInfo(currentToken).username();
UserManager::UserError status = m_userManager->changePassword(username, newPassword);
ret.insert("error", enumValueName<UserManager::UserError>(status));
return createReply(ret);
}
JsonReply *UsersHandler::Authenticate(const QVariantMap &params)
{
QString username = params.value("username").toString();
QString password = params.value("password").toString();
QString deviceName = params.value("deviceName").toString();
QByteArray token = m_userManager->authenticate(username, password, deviceName);
QVariantMap ret;
ret.insert("success", !token.isEmpty());
if (!token.isEmpty()) {
ret.insert("token", token);
}
return createReply(ret);
}
JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap &params)
{
QString deviceName = params.value("deviceName").toString();
QUuid clientId = this->property("clientId").toUuid();
int transactionId = m_userManager->requestPushButtonAuth(deviceName);
m_pushButtonTransactions.insert(transactionId, clientId);
QVariantMap data;
data.insert("transactionId", transactionId);
// TODO: return false if pushbutton auth is disabled in settings
data.insert("success", true);
return createReply(data);
}
JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot get user info form an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
UserInfo userInfo = m_userManager->userInfo(currentToken);
ret.insert("userInfo", pack(userInfo));
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret);
}
JsonReply *UsersHandler::GetTokens(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot fetch tokens form an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
qCDebug(dcJsonRpc()) << "Fetching tokens for user" << tokenInfo.username();
QList<TokenInfo> tokens = m_userManager->tokens(tokenInfo.username());
QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo);
}
ret.insert("tokenInfoList", retList);
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret);
}
JsonReply *UsersHandler::RemoveToken(const QVariantMap &params)
{
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection.";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
QUuid tokenId = params.value("tokenId").toUuid();
TokenInfo tokenToRemove = m_userManager->tokenInfo(tokenId);
if (tokenToRemove.id().isNull()) {
qCWarning(dcJsonRpc()) << "Token with ID" << tokenId << "not found";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorTokenNotFound));
return createReply(ret);
}
TokenInfo currentTokenInfo = m_userManager->tokenInfo(currentToken);
if (currentTokenInfo.username() != tokenToRemove.username()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from another user!";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
}
qCDebug(dcJsonRpc()) << "Removing token" << tokenId << "for user" << currentTokenInfo.username();
UserManager::UserError error = m_userManager->removeToken(tokenId);
ret.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(ret);
}
}

View File

@ -0,0 +1,39 @@
#ifndef USERSHANDLER_H
#define USERSHANDLER_H
#include <QObject>
#include "jsonrpc/jsonhandler.h"
namespace nymeaserver {
class UserManager;
class UsersHandler : public JsonHandler
{
Q_OBJECT
public:
explicit UsersHandler(UserManager *userManager, QObject *parent = nullptr);
QString name() const override;
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap &params);
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params);
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetTokens(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params);
signals:
void PushButtonAuthFinished(const QVariantMap &params);
private:
UserManager *m_userManager = nullptr;
QHash<int, QUuid> m_pushButtonTransactions;
};
}
#endif // USERSHANDLER_H

View File

@ -19,8 +19,6 @@ HEADERS += nymeacore.h \
devices/devicemanagerimplementation.h \ devices/devicemanagerimplementation.h \
devices/translator.h \ devices/translator.h \
experiences/experiencemanager.h \ experiences/experiencemanager.h \
jsonrpc/jsonrpcserverimplementation.h \
jsonrpc/scriptshandler.h \
ruleengine/ruleengine.h \ ruleengine/ruleengine.h \
ruleengine/rule.h \ ruleengine/rule.h \
ruleengine/stateevaluator.h \ ruleengine/stateevaluator.h \
@ -43,6 +41,7 @@ HEADERS += nymeacore.h \
servers/bluetoothserver.h \ servers/bluetoothserver.h \
servers/websocketserver.h \ servers/websocketserver.h \
servers/mqttbroker.h \ servers/mqttbroker.h \
jsonrpc/jsonrpcserverimplementation.h \
jsonrpc/jsonvalidator.h \ jsonrpc/jsonvalidator.h \
jsonrpc/devicehandler.h \ jsonrpc/devicehandler.h \
jsonrpc/ruleshandler.h \ jsonrpc/ruleshandler.h \
@ -52,6 +51,10 @@ HEADERS += nymeacore.h \
jsonrpc/logginghandler.h \ jsonrpc/logginghandler.h \
jsonrpc/configurationhandler.h \ jsonrpc/configurationhandler.h \
jsonrpc/networkmanagerhandler.h \ jsonrpc/networkmanagerhandler.h \
jsonrpc/tagshandler.h \
jsonrpc/systemhandler.h \
jsonrpc/scriptshandler.h \
jsonrpc/usershandler.h \
logging/logging.h \ logging/logging.h \
logging/logengine.h \ logging/logengine.h \
logging/logfilter.h \ logging/logfilter.h \
@ -66,6 +69,7 @@ HEADERS += nymeacore.h \
networkmanager/networksettings.h \ networkmanager/networksettings.h \
networkmanager/networkconnection.h \ networkmanager/networkconnection.h \
networkmanager/wirednetworkdevice.h \ networkmanager/wirednetworkdevice.h \
usermanager/userinfo.h \
usermanager/usermanager.h \ usermanager/usermanager.h \
usermanager/tokeninfo.h \ usermanager/tokeninfo.h \
usermanager/pushbuttondbusservice.h \ usermanager/pushbuttondbusservice.h \
@ -90,18 +94,15 @@ HEADERS += nymeacore.h \
debugserverhandler.h \ debugserverhandler.h \
tagging/tagsstorage.h \ tagging/tagsstorage.h \
tagging/tag.h \ tagging/tag.h \
jsonrpc/tagshandler.h \
cloud/cloudtransport.h \ cloud/cloudtransport.h \
debugreportgenerator.h \ debugreportgenerator.h \
platform/platform.h \ platform/platform.h \
jsonrpc/systemhandler.h
SOURCES += nymeacore.cpp \ SOURCES += nymeacore.cpp \
devices/devicemanagerimplementation.cpp \ devices/devicemanagerimplementation.cpp \
devices/translator.cpp \ devices/translator.cpp \
experiences/experiencemanager.cpp \ experiences/experiencemanager.cpp \
jsonrpc/jsonrpcserverimplementation.cpp \
jsonrpc/scriptshandler.cpp \
ruleengine/ruleengine.cpp \ ruleengine/ruleengine.cpp \
ruleengine/rule.cpp \ ruleengine/rule.cpp \
ruleengine/stateevaluator.cpp \ ruleengine/stateevaluator.cpp \
@ -124,6 +125,7 @@ SOURCES += nymeacore.cpp \
servers/websocketserver.cpp \ servers/websocketserver.cpp \
servers/bluetoothserver.cpp \ servers/bluetoothserver.cpp \
servers/mqttbroker.cpp \ servers/mqttbroker.cpp \
jsonrpc/jsonrpcserverimplementation.cpp \
jsonrpc/jsonvalidator.cpp \ jsonrpc/jsonvalidator.cpp \
jsonrpc/devicehandler.cpp \ jsonrpc/devicehandler.cpp \
jsonrpc/ruleshandler.cpp \ jsonrpc/ruleshandler.cpp \
@ -133,6 +135,10 @@ SOURCES += nymeacore.cpp \
jsonrpc/logginghandler.cpp \ jsonrpc/logginghandler.cpp \
jsonrpc/configurationhandler.cpp \ jsonrpc/configurationhandler.cpp \
jsonrpc/networkmanagerhandler.cpp \ jsonrpc/networkmanagerhandler.cpp \
jsonrpc/tagshandler.cpp \
jsonrpc/systemhandler.cpp \
jsonrpc/scriptshandler.cpp \
jsonrpc/usershandler.cpp \
logging/logengine.cpp \ logging/logengine.cpp \
logging/logfilter.cpp \ logging/logfilter.cpp \
logging/logentry.cpp \ logging/logentry.cpp \
@ -145,6 +151,7 @@ SOURCES += nymeacore.cpp \
networkmanager/networksettings.cpp \ networkmanager/networksettings.cpp \
networkmanager/networkconnection.cpp \ networkmanager/networkconnection.cpp \
networkmanager/wirednetworkdevice.cpp \ networkmanager/wirednetworkdevice.cpp \
usermanager/userinfo.cpp \
usermanager/usermanager.cpp \ usermanager/usermanager.cpp \
usermanager/tokeninfo.cpp \ usermanager/tokeninfo.cpp \
usermanager/pushbuttondbusservice.cpp \ usermanager/pushbuttondbusservice.cpp \
@ -169,11 +176,10 @@ SOURCES += nymeacore.cpp \
debugserverhandler.cpp \ debugserverhandler.cpp \
tagging/tagsstorage.cpp \ tagging/tagsstorage.cpp \
tagging/tag.cpp \ tagging/tag.cpp \
jsonrpc/tagshandler.cpp \
cloud/cloudtransport.cpp \ cloud/cloudtransport.cpp \
debugreportgenerator.cpp \ debugreportgenerator.cpp \
platform/platform.cpp \ platform/platform.cpp \
jsonrpc/systemhandler.cpp
versionAtLeast(QT_VERSION, 5.12.0) { versionAtLeast(QT_VERSION, 5.12.0) {
HEADERS += \ HEADERS += \

View File

@ -42,6 +42,8 @@
#include "tokeninfo.h" #include "tokeninfo.h"
#include <QVariant>
namespace nymeaserver { namespace nymeaserver {
TokenInfo::TokenInfo() TokenInfo::TokenInfo()
@ -83,4 +85,14 @@ QString TokenInfo::deviceName() const
return m_deviceName; return m_deviceName;
} }
QVariant TokenInfoList::get(int index) const
{
return QVariant::fromValue(at(index));
}
void TokenInfoList::put(const QVariant &variant)
{
append(variant.value<TokenInfo>());
}
} }

View File

@ -34,6 +34,7 @@
#include <QUuid> #include <QUuid>
#include <QDateTime> #include <QDateTime>
#include <QMetaType> #include <QMetaType>
#include <QVariant>
namespace nymeaserver { namespace nymeaserver {
@ -43,7 +44,7 @@ class TokenInfo
Q_PROPERTY(QUuid id READ id) Q_PROPERTY(QUuid id READ id)
Q_PROPERTY(QString username READ username) Q_PROPERTY(QString username READ username)
Q_PROPERTY(QDateTime creationTime READ creationTime) Q_PROPERTY(QDateTime creationTime READ creationTime)
Q_PROPERTY(QString deviveName READ deviceName) Q_PROPERTY(QString deviceName READ deviceName)
public: public:
TokenInfo(); TokenInfo();
@ -61,7 +62,17 @@ private:
QString m_deviceName; QString m_deviceName;
}; };
}
Q_DECLARE_METATYPE(nymeaserver::TokenInfo)
class TokenInfoList: public QList<TokenInfo>
{
Q_GADGET
Q_PROPERTY(int count READ count)
public:
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
};
}
Q_DECLARE_METATYPE(nymeaserver::TokenInfo)
Q_DECLARE_METATYPE(nymeaserver::TokenInfoList)
#endif // TOKENINFO_H #endif // TOKENINFO_H

View File

@ -0,0 +1,22 @@
#include "userinfo.h"
UserInfo::UserInfo()
{
}
UserInfo::UserInfo(const QString &username):
m_username(username)
{
}
QString UserInfo::username() const
{
return m_username;
}
void UserInfo::setUsername(const QString &username)
{
m_username = username;
}

View File

@ -0,0 +1,53 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef USERINFO_H
#define USERINFO_H
#include <QUuid>
#include <QObject>
class UserInfo
{
Q_GADGET
Q_PROPERTY(QString username READ username)
public:
UserInfo();
UserInfo(const QString &username);
QString username() const;
void setUsername(const QString &username);
private:
QString m_username;
};
#endif // USERINFO_H

View File

@ -167,7 +167,7 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
QByteArray salt = QUuid::createUuid().toString().remove(QRegExp("[{}]")).toUtf8(); QByteArray salt = QUuid::createUuid().toString().remove(QRegExp("[{}]")).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64(); QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QString queryString = QString("INSERT INTO users(username, password, salt) values(\"%1\", \"%2\", \"%3\");") QString queryString = QString("INSERT INTO users(username, password, salt) values(\"%1\", \"%2\", \"%3\");")
.arg(username) .arg(username.toLower())
.arg(QString::fromUtf8(hashedPassword)) .arg(QString::fromUtf8(hashedPassword))
.arg(QString::fromUtf8(salt)); .arg(QString::fromUtf8(salt));
m_db.exec(queryString); m_db.exec(queryString);
@ -178,9 +178,41 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
return UserErrorNoError; return UserErrorNoError;
} }
/*! Remove the user with the given \a username and all of its tokens. If the \a username is empty, all anonymous UserManager::UserError UserManager::changePassword(const QString &username, const QString &newPassword)
tokens (e.g. issued by pushbutton auth) will be cleared. {
*/ if (!validateUsername(username)) {
qCWarning(dcUserManager) << "Invalid username:" << username;
return UserErrorInvalidUserId;
}
if (!validatePassword(newPassword)) {
qCWarning(dcUserManager) << "Password failed character validation. Must contain a letter, a number and a special charactar. Minimum length: 8";
return UserErrorBadPassword;
}
QString checkForUserExistingQuery = QString("SELECT * FROM users WHERE lower(username) = \"%1\";").arg(username.toLower());
QSqlQuery result = m_db.exec(checkForUserExistingQuery);
if (!result.first()) {
qCWarning(dcUserManager) << "Username does not exist.";
return UserErrorInvalidUserId;
}
// Update the password
QByteArray salt = QUuid::createUuid().toString().remove(QRegExp("[{}]")).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(newPassword + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QString queryString = QString("UPDATE users SET password = \"%1\", salt = \"%2\" WHERE lower(username) = \"%3\";")
.arg(QString::fromUtf8(hashedPassword))
.arg(QString::fromUtf8(salt))
.arg(username.toLower());
m_db.exec(queryString);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error updating password for user:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserErrorBackendError;
}
qCDebug(dcUserManager()) << "Password updated for user" << username;
return UserErrorNoError;
}
UserManager::UserError UserManager::removeUser(const QString &username) UserManager::UserError UserManager::removeUser(const QString &username)
{ {
if (!username.isEmpty()) { if (!username.isEmpty()) {
@ -189,14 +221,11 @@ UserManager::UserError UserManager::removeUser(const QString &username)
if (result.numRowsAffected() == 0) { if (result.numRowsAffected() == 0) {
return UserErrorInvalidUserId; return UserErrorInvalidUserId;
} }
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);
} }
QString dropTokensQuery = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower());
m_db.exec(dropTokensQuery);
return UserErrorNoError; return UserErrorNoError;
} }
@ -233,7 +262,7 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");") QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
.arg(QUuid::createUuid().toString()) .arg(QUuid::createUuid().toString())
.arg(username) .arg(username.toLower())
.arg(QString::fromUtf8(token)) .arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) .arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(deviceName); .arg(deviceName);
@ -280,32 +309,32 @@ void UserManager::cancelPushButtonAuth(int transactionId)
} }
/*! Returns the username for the given \a token. If the token is invalid, an empty string will be returned. */ UserInfo UserManager::userInfo(const QByteArray &token) const
QString UserManager::userForToken(const QByteArray &token) const
{ {
if (!validateToken(token)) { TokenInfo tokenInfo = this->tokenInfo(token);
qCWarning(dcUserManager) << "Token failed character validation:" << token;
return QString(); if (tokenInfo.id().isNull()) {
qCWarning(dcUserManager) << "Cannot fetch user info for invalid token:" << token;
return UserInfo();
} }
QString getUserQuery = QString("SELECT * FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token)); // OK, this seems pointless, but data structures are prepared to have more details about users than just the username
// i.e. permissions etc will be in here at some point
QString getUserQuery = QString("SELECT username FROM users WHERE lower(username) = \"%1\";")
.arg(tokenInfo.username().toLower());
QSqlQuery result = m_db.exec(getUserQuery); QSqlQuery result = m_db.exec(getUserQuery);
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error fetching username for token:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery; qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery;
return QString(); return UserInfo();
}
if (!result.first()) {
qCWarning(dcUserManager) << "No such token in DB:" << token;
return QString();
} }
return result.value("username").toString(); if (!result.first()) {
return UserInfo();
}
return UserInfo(result.value("username").toString());
} }
/*! Returns a list of tokens for the given \a username.
\sa TokenInfo
*/
QList<TokenInfo> UserManager::tokens(const QString &username) const QList<TokenInfo> UserManager::tokens(const QString &username) const
{ {
QList<TokenInfo> ret; QList<TokenInfo> ret;
@ -328,6 +357,44 @@ QList<TokenInfo> UserManager::tokens(const QString &username) const
return ret; return ret;
} }
TokenInfo UserManager::tokenInfo(const QByteArray &token) const
{
if (!validateToken(token)) {
qCWarning(dcUserManager) << "Token did not pass validation:" << token;
return TokenInfo();
}
QString getTokenQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
QSqlQuery result = m_db.exec(getTokenQuery);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQuery;
return TokenInfo();
}
if (!result.first()) {
return TokenInfo();
}
return TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString());
}
TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const
{
QString getTokenQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
QSqlQuery result = m_db.exec(getTokenQuery);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQuery;
return TokenInfo();
}
if (!result.first()) {
return TokenInfo();
}
return TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString());
}
/*! Removes the token with the given \a tokenId. Returns \l{UserError} to inform about the result. */ /*! Removes the token with the given \a tokenId. Returns \l{UserError} to inform about the result. */
UserManager::UserError UserManager::removeToken(const QUuid &tokenId) UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
{ {

View File

@ -32,6 +32,7 @@
#define USERMANAGER_H #define USERMANAGER_H
#include "tokeninfo.h" #include "tokeninfo.h"
#include "userinfo.h"
#include <QObject> #include <QObject>
#include <QSqlDatabase> #include <QSqlDatabase>
@ -61,6 +62,7 @@ public:
QStringList users() const; QStringList users() const;
UserError createUser(const QString &username, const QString &password); UserError createUser(const QString &username, const QString &password);
UserError changePassword(const QString &username, const QString &newPassword);
UserError removeUser(const QString &username); UserError removeUser(const QString &username);
bool pushButtonAuthAvailable() const; bool pushButtonAuthAvailable() const;
@ -68,9 +70,14 @@ public:
QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName); QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName);
int requestPushButtonAuth(const QString &deviceName); int requestPushButtonAuth(const QString &deviceName);
void cancelPushButtonAuth(int transactionId); void cancelPushButtonAuth(int transactionId);
QString userForToken(const QByteArray &token) const;
UserInfo userInfo(const QByteArray &token) const;
TokenInfo tokenInfo(const QByteArray &token) const;
TokenInfo tokenInfo(const QUuid &tokenId) const;
QList<TokenInfo> tokens(const QString &username) const; QList<TokenInfo> tokens(const QString &username) const;
nymeaserver::UserManager::UserError removeToken(const QUuid &tokenId);
UserError removeToken(const QUuid &tokenId);
bool verifyToken(const QByteArray &token); bool verifyToken(const QByteArray &token);

View File

@ -5,7 +5,7 @@ NYMEA_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p"
# define protocol versions # define protocol versions
JSON_PROTOCOL_VERSION_MAJOR=4 JSON_PROTOCOL_VERSION_MAJOR=4
JSON_PROTOCOL_VERSION_MINOR=1 JSON_PROTOCOL_VERSION_MINOR=2
JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}" JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}"
LIBNYMEA_API_VERSION_MAJOR=4 LIBNYMEA_API_VERSION_MAJOR=4
LIBNYMEA_API_VERSION_MINOR=0 LIBNYMEA_API_VERSION_MINOR=0
@ -105,3 +105,4 @@ coverage {
ccache { ccache {
message("Using ccache.") message("Using ccache.")
} }

View File

@ -1,4 +1,4 @@
4.1 4.2
{ {
"enums": { "enums": {
"BasicType": [ "BasicType": [
@ -920,6 +920,7 @@
} }
}, },
"JSONRPC.RemoveToken": { "JSONRPC.RemoveToken": {
"deprecated": "Use Users.RemoveToken instead.",
"description": "Revoke access for a given token.", "description": "Revoke access for a given token.",
"params": { "params": {
"tokenId": "Uuid" "tokenId": "Uuid"
@ -974,6 +975,7 @@
} }
}, },
"JSONRPC.Tokens": { "JSONRPC.Tokens": {
"deprecated": "Use Users.GetTokens instead.",
"description": "Return a list of TokenInfo objects of all the tokens for the current user.", "description": "Return a list of TokenInfo objects of all the tokens for the current user.",
"params": { "params": {
}, },
@ -1464,6 +1466,52 @@
"returns": { "returns": {
"tagError": "$ref:TagError" "tagError": "$ref:TagError"
} }
},
"Users.ChangePassword": {
"description": "Change the password for the currently logged in user.",
"params": {
"newPassword": "String"
},
"returns": {
"error": "$ref:UserError"
}
},
"Users.CreateUser": {
"description": "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user.",
"params": {
"password": "String",
"username": "String"
},
"returns": {
"error": "$ref:UserError"
}
},
"Users.GetTokens": {
"description": "Get all the tokens for the current user.",
"params": {
},
"returns": {
"error": "$ref:UserError",
"o:tokenInfoList": "$ref:TokenInfoList"
}
},
"Users.GetUserInfo": {
"description": "Get info about the current token (the currently logged in user).",
"params": {
},
"returns": {
"error": "$ref:UserError",
"o:userInfo": "$ref:UserInfo"
}
},
"Users.RemoveToken": {
"description": "Revoke access for a given token.",
"params": {
"tokenId": "Uuid"
},
"returns": {
"error": "$ref:UserError"
}
} }
}, },
"notifications": { "notifications": {
@ -2147,10 +2195,16 @@
], ],
"TokenInfo": { "TokenInfo": {
"r:creationTime": "Uint", "r:creationTime": "Uint",
"r:deviveName": "String", "r:deviceName": "String",
"r:id": "Uuid", "r:id": "Uuid",
"r:username": "String" "r:username": "String"
}, },
"TokenInfoList": [
"$ref:TokenInfo"
],
"UserInfo": {
"r:username": "String"
},
"Vendor": { "Vendor": {
"displayName": "String", "displayName": "String",
"id": "Uuid", "id": "Uuid",

View File

@ -675,7 +675,7 @@ void TestJSONRPC::enableDisableNotifications_legacy()
QStringList expectedNamespaces; QStringList expectedNamespaces;
if (enabled == "true") { if (enabled == "true") {
expectedNamespaces << "Actions" << "NetworkManager" << "Devices" << "System" << "Rules" << "States" << "Logging" << "Tags" << "JSONRPC" << "Configuration" << "Events" << "Scripts"; expectedNamespaces << "Actions" << "NetworkManager" << "Devices" << "System" << "Rules" << "States" << "Logging" << "Tags" << "JSONRPC" << "Configuration" << "Events" << "Scripts" << "Users";
} }
std::sort(expectedNamespaces.begin(), expectedNamespaces.end()); std::sort(expectedNamespaces.begin(), expectedNamespaces.end());

View File

@ -34,6 +34,7 @@
#include "nymeacore.h" #include "nymeacore.h"
#include "nymeatestbase.h" #include "nymeatestbase.h"
#include "usermanager/usermanager.h" #include "usermanager/usermanager.h"
#include "servers/mocktcpserver.h"
using namespace nymeaserver; using namespace nymeaserver;
@ -44,11 +45,33 @@ public:
TestUsermanager(QObject* parent = nullptr); TestUsermanager(QObject* parent = nullptr);
private slots: private slots:
void createUser_data(); void init();
void loginValidation_data();
void loginValidation();
void createUser(); void createUser();
void authenticate();
void createDuplicateUser();
void getTokens();
void removeToken();
void unauthenticatedCallAfterTokenRemove();
void changePassword();
void authenticateAfterPasswordChangeOK();
void authenticateAfterPasswordChangeFail();
void getUserInfo();
private: private:
LogEngine *engine; // m_apiToken is in testBase
QUuid m_tokenId;
}; };
TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent) TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
@ -56,7 +79,18 @@ TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
QCoreApplication::instance()->setOrganizationName("nymea-test"); QCoreApplication::instance()->setOrganizationName("nymea-test");
} }
void TestUsermanager::createUser_data() { void TestUsermanager::init()
{
UserManager *userManager = NymeaCore::instance()->userManager();
foreach (const QString &user, userManager->users()) {
qCDebug(dcTests()) << "Removing user" << user;
userManager->removeUser(user);
}
userManager->removeUser("");
}
void TestUsermanager::loginValidation_data() {
QTest::addColumn<QString>("username"); QTest::addColumn<QString>("username");
QTest::addColumn<QString>("password"); QTest::addColumn<QString>("password");
QTest::addColumn<UserManager::UserError>("expectedError"); QTest::addColumn<UserManager::UserError>("expectedError");
@ -88,23 +122,163 @@ void TestUsermanager::createUser_data() {
} }
void TestUsermanager::createUser() void TestUsermanager::loginValidation()
{ {
QFETCH(QString, username); QFETCH(QString, username);
QFETCH(QString, password); QFETCH(QString, password);
QFETCH(UserManager::UserError, expectedError); QFETCH(UserManager::UserError, expectedError);
UserManager *userManager = NymeaCore::instance()->userManager(); UserManager *userManager = NymeaCore::instance()->userManager();
foreach (const QString &user, userManager->users()) {
userManager->removeUser(user);
}
userManager->removeUser("");
UserManager::UserError error = userManager->createUser(username, password); UserManager::UserError error = userManager->createUser(username, password);
qDebug() << "Error:" << error << "Expected:" << expectedError; qDebug() << "Error:" << error << "Expected:" << expectedError;
QCOMPARE(error, expectedError); QCOMPARE(error, expectedError);
} }
void TestUsermanager::createUser()
{
QVariantMap params;
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*");
QVariant response = injectAndWait("Users.CreateUser", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Error creating user");
QVERIFY2(response.toMap().value("params").toMap().value("error").toString() == "UserErrorNoError", "Error creating user");
}
void TestUsermanager::authenticate()
{
m_apiToken.clear();
createUser();
QVariantMap params;
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*");
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating");
QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating");
}
void TestUsermanager::createDuplicateUser()
{
authenticate();
QVariantMap params;
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*");
QVariant response = injectAndWait("Users.CreateUser", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Unexpected error code creating duplicate user");
QVERIFY2(response.toMap().value("params").toMap().value("error").toString() == "UserErrorDuplicateUserId", "Unexpected error creating duplicate user");
}
void TestUsermanager::getTokens()
{
authenticate();
QVariant response = injectAndWait("Users.GetTokens");
QVERIFY2(response.toMap().value("status").toString() == "success", "Unexpected error code creating duplicate user");
QCOMPARE(response.toMap().value("params").toMap().value("error").toString(), QString("UserErrorNoError"));
QVariantList tokenInfoList = response.toMap().value("params").toMap().value("tokenInfoList").toList();
QCOMPARE(tokenInfoList.count(), 1);
m_tokenId = tokenInfoList.first().toMap().value("id").toUuid();
QVERIFY2(!m_tokenId.isNull(), "Token ID should not be null");
QCOMPARE(tokenInfoList.first().toMap().value("username").toString(), QString("valid@user.test"));
QCOMPARE(tokenInfoList.first().toMap().value("deviceName").toString(), QString("autotests"));
}
void TestUsermanager::removeToken()
{
getTokens();
QVariantMap params;
params.insert("tokenId", m_tokenId);
QVariant response = injectAndWait("Users.RemoveToken", params);
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QCOMPARE(response.toMap().value("params").toMap().value("error").toString(), QString("UserErrorNoError"));
}
void TestUsermanager::changePassword()
{
authenticate();
QVariantMap params;
params.insert("newPassword", "Blubb123");
QVariant response = injectAndWait("Users.ChangePassword", params);
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QCOMPARE(response.toMap().value("params").toMap().value("error").toString(), QString("UserErrorNoError"));
}
void TestUsermanager::authenticateAfterPasswordChangeOK()
{
changePassword();
QVariantMap params;
params.insert("username", "valid@user.test");
params.insert("password", "Blubb123"); // New password, should be ok
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
QVERIFY2(!m_apiToken.isEmpty(), "Token should not be empty");
QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating");
QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating");
}
void TestUsermanager::authenticateAfterPasswordChangeFail()
{
changePassword();
QVariantMap params;
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*"); // Original password, should not be ok
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
QVERIFY2(m_apiToken.isEmpty(), "Token should be empty");
QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating");
QCOMPARE(response.toMap().value("params").toMap().value("success").toString(), QString("false"));
}
void TestUsermanager::getUserInfo()
{
authenticate();
QVariant response = injectAndWait("Users.GetUserInfo");
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QVariantMap userInfoMap = response.toMap().value("params").toMap().value("userInfo").toMap();
QCOMPARE(userInfoMap.value("username").toString(), QString("valid@user.test"));
}
void TestUsermanager::unauthenticatedCallAfterTokenRemove()
{
removeToken();
QSignalSpy spy(m_mockTcpServer, &MockTcpServer::connectionTerminated);
QVariant response = injectAndWait("Users.GetTokens");
QCOMPARE(response.toMap().value("status").toString(), QString("unauthorized"));
if (spy.count() == 0) {
spy.wait();
}
QVERIFY2(spy.count() == 1, "Connection should be terminated!");
// need to restart as our connection dies
restartServer();
}
#include "testusermanager.moc" #include "testusermanager.moc"
QTEST_MAIN(TestUsermanager) QTEST_MAIN(TestUsermanager)