Add more user management features

pull/257/head
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 "tagshandler.h"
#include "systemhandler.h"
#include "usershandler.h"
#include <QJsonDocument>
#include <QStringList>
@ -194,13 +195,13 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.clear(); returns.clear();
description = "Return a list of TokenInfo objects of all the tokens for the current user.";
returns.insert("tokenInfoList", QVariantList() << objectRef("TokenInfo"));
registerMethod("Tokens", description, params, returns);
registerMethod("Tokens", description, params, returns, "Use Users.GetTokens instead.");
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);
registerMethod("RemoveToken", description, params, returns, "Use Users.RemoveToken instead.");
params.clear(); returns.clear();
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)
QByteArray token = property("token").toByteArray();
QString username = NymeaCore::instance()->userManager()->userForToken(token);
QList<TokenInfo> tokens = NymeaCore::instance()->userManager()->tokens(username);
TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token);
QList<TokenInfo> tokens = NymeaCore::instance()->userManager()->tokens(tokenInfo.username());
QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo);
@ -569,6 +570,7 @@ void JsonRPCServerImplementation::setup()
registerHandler(new NetworkManagerHandler(this));
registerHandler(new TagsHandler(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::connectionStateChanged, this, &JsonRPCServerImplementation::onCloudConnectionStateChanged);
@ -634,19 +636,19 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
// check if authentication is required for this transport
if (m_interfaces.value(interface)) {
QByteArray token = message.value("token").toByteArray();
QStringList authExemptMethodsNoUser = {"Introspect", "Hello", "CreateUser", "RequestPushButtonAuth"};
QStringList authExemptMethodsWithUser = {"Introspect", "Hello", "Authenticate", "RequestPushButtonAuth"};
QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.CreateUser"};
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 (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.");
if (!authExemptMethodsNoUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
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.";
interface->terminateClientConnection(clientId);
return;
}
} else {
// 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.");
qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection.";
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/translator.h \
experiences/experiencemanager.h \
jsonrpc/jsonrpcserverimplementation.h \
jsonrpc/scriptshandler.h \
ruleengine/ruleengine.h \
ruleengine/rule.h \
ruleengine/stateevaluator.h \
@ -43,6 +41,7 @@ HEADERS += nymeacore.h \
servers/bluetoothserver.h \
servers/websocketserver.h \
servers/mqttbroker.h \
jsonrpc/jsonrpcserverimplementation.h \
jsonrpc/jsonvalidator.h \
jsonrpc/devicehandler.h \
jsonrpc/ruleshandler.h \
@ -52,6 +51,10 @@ HEADERS += nymeacore.h \
jsonrpc/logginghandler.h \
jsonrpc/configurationhandler.h \
jsonrpc/networkmanagerhandler.h \
jsonrpc/tagshandler.h \
jsonrpc/systemhandler.h \
jsonrpc/scriptshandler.h \
jsonrpc/usershandler.h \
logging/logging.h \
logging/logengine.h \
logging/logfilter.h \
@ -66,6 +69,7 @@ HEADERS += nymeacore.h \
networkmanager/networksettings.h \
networkmanager/networkconnection.h \
networkmanager/wirednetworkdevice.h \
usermanager/userinfo.h \
usermanager/usermanager.h \
usermanager/tokeninfo.h \
usermanager/pushbuttondbusservice.h \
@ -90,18 +94,15 @@ HEADERS += nymeacore.h \
debugserverhandler.h \
tagging/tagsstorage.h \
tagging/tag.h \
jsonrpc/tagshandler.h \
cloud/cloudtransport.h \
debugreportgenerator.h \
platform/platform.h \
jsonrpc/systemhandler.h
SOURCES += nymeacore.cpp \
devices/devicemanagerimplementation.cpp \
devices/translator.cpp \
experiences/experiencemanager.cpp \
jsonrpc/jsonrpcserverimplementation.cpp \
jsonrpc/scriptshandler.cpp \
ruleengine/ruleengine.cpp \
ruleengine/rule.cpp \
ruleengine/stateevaluator.cpp \
@ -124,6 +125,7 @@ SOURCES += nymeacore.cpp \
servers/websocketserver.cpp \
servers/bluetoothserver.cpp \
servers/mqttbroker.cpp \
jsonrpc/jsonrpcserverimplementation.cpp \
jsonrpc/jsonvalidator.cpp \
jsonrpc/devicehandler.cpp \
jsonrpc/ruleshandler.cpp \
@ -133,6 +135,10 @@ SOURCES += nymeacore.cpp \
jsonrpc/logginghandler.cpp \
jsonrpc/configurationhandler.cpp \
jsonrpc/networkmanagerhandler.cpp \
jsonrpc/tagshandler.cpp \
jsonrpc/systemhandler.cpp \
jsonrpc/scriptshandler.cpp \
jsonrpc/usershandler.cpp \
logging/logengine.cpp \
logging/logfilter.cpp \
logging/logentry.cpp \
@ -145,6 +151,7 @@ SOURCES += nymeacore.cpp \
networkmanager/networksettings.cpp \
networkmanager/networkconnection.cpp \
networkmanager/wirednetworkdevice.cpp \
usermanager/userinfo.cpp \
usermanager/usermanager.cpp \
usermanager/tokeninfo.cpp \
usermanager/pushbuttondbusservice.cpp \
@ -169,11 +176,10 @@ SOURCES += nymeacore.cpp \
debugserverhandler.cpp \
tagging/tagsstorage.cpp \
tagging/tag.cpp \
jsonrpc/tagshandler.cpp \
cloud/cloudtransport.cpp \
debugreportgenerator.cpp \
platform/platform.cpp \
jsonrpc/systemhandler.cpp
versionAtLeast(QT_VERSION, 5.12.0) {
HEADERS += \

View File

@ -42,6 +42,8 @@
#include "tokeninfo.h"
#include <QVariant>
namespace nymeaserver {
TokenInfo::TokenInfo()
@ -83,4 +85,14 @@ QString TokenInfo::deviceName() const
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 <QDateTime>
#include <QMetaType>
#include <QVariant>
namespace nymeaserver {
@ -43,7 +44,7 @@ class TokenInfo
Q_PROPERTY(QUuid id READ id)
Q_PROPERTY(QString username READ username)
Q_PROPERTY(QDateTime creationTime READ creationTime)
Q_PROPERTY(QString deviveName READ deviceName)
Q_PROPERTY(QString deviceName READ deviceName)
public:
TokenInfo();
@ -61,7 +62,17 @@ private:
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

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 hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
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(salt));
m_db.exec(queryString);
@ -178,9 +178,41 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
return UserErrorNoError;
}
/*! Remove the user with the given \a username and all of its tokens. If the \a username is empty, all anonymous
tokens (e.g. issued by pushbutton auth) will be cleared.
*/
UserManager::UserError UserManager::changePassword(const QString &username, const QString &newPassword)
{
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)
{
if (!username.isEmpty()) {
@ -189,14 +221,11 @@ UserManager::UserError UserManager::removeUser(const QString &username)
if (result.numRowsAffected() == 0) {
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;
}
@ -233,7 +262,7 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
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\");")
.arg(QUuid::createUuid().toString())
.arg(username)
.arg(username.toLower())
.arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.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. */
QString UserManager::userForToken(const QByteArray &token) const
UserInfo UserManager::userInfo(const QByteArray &token) const
{
if (!validateToken(token)) {
qCWarning(dcUserManager) << "Token failed character validation:" << token;
return QString();
TokenInfo tokenInfo = this->tokenInfo(token);
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);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error fetching username for token:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery;
return QString();
}
if (!result.first()) {
qCWarning(dcUserManager) << "No such token in DB:" << token;
return QString();
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery;
return UserInfo();
}
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> ret;
@ -328,6 +357,44 @@ QList<TokenInfo> UserManager::tokens(const QString &username) const
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. */
UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
{

View File

@ -32,6 +32,7 @@
#define USERMANAGER_H
#include "tokeninfo.h"
#include "userinfo.h"
#include <QObject>
#include <QSqlDatabase>
@ -61,6 +62,7 @@ public:
QStringList users() const;
UserError createUser(const QString &username, const QString &password);
UserError changePassword(const QString &username, const QString &newPassword);
UserError removeUser(const QString &username);
bool pushButtonAuthAvailable() const;
@ -68,9 +70,14 @@ public:
QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName);
int requestPushButtonAuth(const QString &deviceName);
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;
nymeaserver::UserManager::UserError removeToken(const QUuid &tokenId);
UserError removeToken(const QUuid &tokenId);
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
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}"
LIBNYMEA_API_VERSION_MAJOR=4
LIBNYMEA_API_VERSION_MINOR=0
@ -105,3 +105,4 @@ coverage {
ccache {
message("Using ccache.")
}

View File

@ -1,4 +1,4 @@
4.1
4.2
{
"enums": {
"BasicType": [
@ -920,6 +920,7 @@
}
},
"JSONRPC.RemoveToken": {
"deprecated": "Use Users.RemoveToken instead.",
"description": "Revoke access for a given token.",
"params": {
"tokenId": "Uuid"
@ -974,6 +975,7 @@
}
},
"JSONRPC.Tokens": {
"deprecated": "Use Users.GetTokens instead.",
"description": "Return a list of TokenInfo objects of all the tokens for the current user.",
"params": {
},
@ -1464,6 +1466,52 @@
"returns": {
"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": {
@ -2147,10 +2195,16 @@
],
"TokenInfo": {
"r:creationTime": "Uint",
"r:deviveName": "String",
"r:deviceName": "String",
"r:id": "Uuid",
"r:username": "String"
},
"TokenInfoList": [
"$ref:TokenInfo"
],
"UserInfo": {
"r:username": "String"
},
"Vendor": {
"displayName": "String",
"id": "Uuid",

View File

@ -675,7 +675,7 @@ void TestJSONRPC::enableDisableNotifications_legacy()
QStringList expectedNamespaces;
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());

View File

@ -34,6 +34,7 @@
#include "nymeacore.h"
#include "nymeatestbase.h"
#include "usermanager/usermanager.h"
#include "servers/mocktcpserver.h"
using namespace nymeaserver;
@ -44,11 +45,33 @@ public:
TestUsermanager(QObject* parent = nullptr);
private slots:
void createUser_data();
void init();
void loginValidation_data();
void loginValidation();
void createUser();
void authenticate();
void createDuplicateUser();
void getTokens();
void removeToken();
void unauthenticatedCallAfterTokenRemove();
void changePassword();
void authenticateAfterPasswordChangeOK();
void authenticateAfterPasswordChangeFail();
void getUserInfo();
private:
LogEngine *engine;
// m_apiToken is in testBase
QUuid m_tokenId;
};
TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
@ -56,7 +79,18 @@ TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
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>("password");
QTest::addColumn<UserManager::UserError>("expectedError");
@ -88,23 +122,163 @@ void TestUsermanager::createUser_data() {
}
void TestUsermanager::createUser()
void TestUsermanager::loginValidation()
{
QFETCH(QString, username);
QFETCH(QString, password);
QFETCH(UserManager::UserError, expectedError);
UserManager *userManager = NymeaCore::instance()->userManager();
foreach (const QString &user, userManager->users()) {
userManager->removeUser(user);
}
userManager->removeUser("");
UserManager::UserError error = userManager->createUser(username, password);
qDebug() << "Error:" << error << "Expected:" << 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"
QTEST_MAIN(TestUsermanager)