Added API authentication
This commit is contained in:
parent
1b08bfb1d7
commit
08727a07ba
@ -38,3 +38,4 @@ Q_LOGGING_CATEGORY(dcOAuth2, "OAuth2")
|
||||
Q_LOGGING_CATEGORY(dcAvahi, "Avahi")
|
||||
Q_LOGGING_CATEGORY(dcCloud, "Cloud")
|
||||
Q_LOGGING_CATEGORY(dcNetworkManager, "NetworkManager")
|
||||
Q_LOGGING_CATEGORY(dcUserManager, "UserManager")
|
||||
|
||||
@ -46,5 +46,6 @@ Q_DECLARE_LOGGING_CATEGORY(dcOAuth2)
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcAvahi)
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcCloud)
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcNetworkManager)
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcUserManager)
|
||||
|
||||
#endif // LOGGINGCATEGORYS_H
|
||||
|
||||
@ -404,6 +404,11 @@ NetworkManager *GuhCore::networkManager() const
|
||||
return m_networkManager;
|
||||
}
|
||||
|
||||
UserManager *GuhCore::userManager() const
|
||||
{
|
||||
return m_userManager;
|
||||
}
|
||||
|
||||
#ifdef TESTING_ENABLED
|
||||
MockTcpServer *GuhCore::tcpServer() const
|
||||
{
|
||||
@ -462,6 +467,8 @@ GuhCore::GuhCore(QObject *parent) :
|
||||
// Create the NetworkManager
|
||||
m_networkManager = new NetworkManager(this);
|
||||
|
||||
m_userManager = new UserManager(this);
|
||||
|
||||
connect(m_configuration, &GuhConfiguration::localeChanged, this, &GuhCore::onLocaleChanged);
|
||||
|
||||
connect(m_deviceManager, &DeviceManager::pluginConfigChanged, this, &GuhCore::pluginConfigChanged);
|
||||
|
||||
@ -86,6 +86,7 @@ public:
|
||||
ServerManager *serverManager() const;
|
||||
BluetoothServer *bluetoothServer() const;
|
||||
NetworkManager *networkManager() const;
|
||||
UserManager *userManager() const;
|
||||
|
||||
static QStringList getAvailableLanguages();
|
||||
|
||||
@ -125,6 +126,7 @@ private:
|
||||
TimeManager *m_timeManager;
|
||||
|
||||
NetworkManager *m_networkManager;
|
||||
UserManager *m_userManager;
|
||||
|
||||
#ifdef TESTING_ENABLED
|
||||
MockTcpServer *m_tcpServer;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2015 Simon Stürz <simon.stuerz@guh.io> *
|
||||
* Copyright (C) 2014 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* Copyright (C) 2014-2017 Michael Zanetti <michael.zanetti@guh.io> *
|
||||
* *
|
||||
* This file is part of guh. *
|
||||
* *
|
||||
@ -95,6 +95,24 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject
|
||||
returns.insert("enabled", JsonTypes::basicTypeToString(JsonTypes::Bool));
|
||||
setReturns("SetNotificationStatus", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
setDescription("CreateUser", "Create a new user in the API. Currently this is only allowed to be called once when a new guh instance is set up. Call Authenticate after this to obtain a device token for this user.");
|
||||
params.insert("username", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("password", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
setParams("CreateUser", params);
|
||||
returns.insert("error", JsonTypes::userErrorRef());
|
||||
setReturns("CreateUser", returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
setDescription("Authenticate", "Authenticate a client to the api. This will return a new token to be used to authorize a client at the API.");
|
||||
params.insert("username", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("password", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
params.insert("deviceName", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
setParams("Authenticate", params);
|
||||
returns.insert("success", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
returns.insert("o:token", JsonTypes::basicTypeToString(JsonTypes::String));
|
||||
setReturns("Authenticate", returns);
|
||||
|
||||
QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
@ -144,6 +162,33 @@ JsonReply* JsonRPCServer::SetNotificationStatus(const QVariantMap ¶ms)
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply *JsonRPCServer::CreateUser(const QVariantMap ¶ms)
|
||||
{
|
||||
QString username = params.value("username").toString();
|
||||
QString password = params.value("password").toString();
|
||||
|
||||
UserManager::UserError status = GuhCore::instance()->userManager()->createUser(username, password);
|
||||
|
||||
QVariantMap returns;
|
||||
returns.insert("error", JsonTypes::userErrorToString(status));
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply *JsonRPCServer::Authenticate(const QVariantMap ¶ms)
|
||||
{
|
||||
QString username = params.value("username").toString();
|
||||
QString password = params.value("password").toString();
|
||||
QString deviceName = params.value("deviceName").toString();
|
||||
|
||||
QByteArray token = GuhCore::instance()->userManager()->authenticate(username, password, deviceName);
|
||||
QVariantMap ret;
|
||||
ret.insert("success", !token.isEmpty());
|
||||
if (!token.isEmpty()) {
|
||||
ret.insert("token", token);
|
||||
}
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
/*! Returns the list of registred \l{JsonHandler}{JsonHandlers} and their name.*/
|
||||
QHash<QString, JsonHandler *> JsonRPCServer::handlers() const
|
||||
{
|
||||
@ -189,6 +234,16 @@ void JsonRPCServer::sendErrorResponse(TransportInterface *interface, const QUuid
|
||||
interface->sendData(clientId, QJsonDocument::fromVariant(errorResponse).toJson());
|
||||
}
|
||||
|
||||
void JsonRPCServer::sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error)
|
||||
{
|
||||
QVariantMap errorResponse;
|
||||
errorResponse.insert("id", commandId);
|
||||
errorResponse.insert("status", "unauthorized");
|
||||
errorResponse.insert("error", error);
|
||||
|
||||
interface->sendData(clientId, QJsonDocument::fromVariant(errorResponse).toJson());
|
||||
}
|
||||
|
||||
void JsonRPCServer::setup()
|
||||
{
|
||||
registerHandler(this);
|
||||
@ -230,10 +285,25 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data)
|
||||
sendErrorResponse(interface, clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
QString targetNamespace = commandList.first();
|
||||
QString method = commandList.last();
|
||||
|
||||
// if there is no user in the system yet, let's fail unless this is a CreateUser or Introspect call
|
||||
if (GuhCore::instance()->userManager()->users().isEmpty()) {
|
||||
if (!(targetNamespace == "JSONRPC" && (method == "CreateUser" || method == "Introspect"))) {
|
||||
sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call CreateUser first.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate or Introspect call
|
||||
QByteArray token = message.value("token").toByteArray();
|
||||
if (!(targetNamespace == "JSONRPC" && (method == "Authenticate" || method == "Introspect")) && !GuhCore::instance()->userManager()->verifyToken(token)) {
|
||||
sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// At this point we can assume all the calls are authorized
|
||||
|
||||
JsonHandler *handler = m_handlers.value(targetNamespace);
|
||||
if (!handler) {
|
||||
sendErrorResponse(interface, clientId, commandId, "No such namespace");
|
||||
@ -324,8 +394,8 @@ void JsonRPCServer::registerHandler(JsonHandler *handler)
|
||||
|
||||
void JsonRPCServer::clientConnected(const QUuid &clientId)
|
||||
{
|
||||
// Notifications enabled by default
|
||||
m_clients.insert(clientId, true);
|
||||
// Notifications disabled by default. Clients must enable them with a valid token
|
||||
m_clients.insert(clientId, false);
|
||||
|
||||
TransportInterface *interface = qobject_cast<TransportInterface *>(sender());
|
||||
|
||||
@ -337,6 +407,7 @@ void JsonRPCServer::clientConnected(const QUuid &clientId)
|
||||
handshake.insert("uuid", GuhCore::instance()->configuration()->serverUuid().toString());
|
||||
handshake.insert("language", GuhCore::instance()->configuration()->locale().name());
|
||||
handshake.insert("protocol version", JSON_PROTOCOL_VERSION);
|
||||
handshake.insert("initialSetupRequired", GuhCore::instance()->userManager()->users().isEmpty());
|
||||
interface->sendData(clientId, QJsonDocument::fromVariant(handshake).toJson());
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2015 Simon Stürz <simon.stuerz@guh.io> *
|
||||
* Copyright (C) 2014 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* Copyright (C) 2014-2017 Michael Zanetti <michael.zanetti@guh.io> *
|
||||
* *
|
||||
* This file is part of guh. *
|
||||
* *
|
||||
@ -25,6 +25,7 @@
|
||||
#include "plugin/deviceclass.h"
|
||||
#include "jsonhandler.h"
|
||||
#include "transportinterface.h"
|
||||
#include "usermanager.h"
|
||||
|
||||
#include "types/action.h"
|
||||
#include "types/event.h"
|
||||
@ -50,6 +51,9 @@ public:
|
||||
Q_INVOKABLE JsonReply *Version(const QVariantMap ¶ms) const;
|
||||
Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap ¶ms);
|
||||
|
||||
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms);
|
||||
|
||||
QHash<QString, JsonHandler *> handlers() const;
|
||||
|
||||
void registerTransportInterface(TransportInterface *interface, const bool &enabled = true);
|
||||
@ -57,6 +61,7 @@ public:
|
||||
private:
|
||||
void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap());
|
||||
void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
|
||||
void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error);
|
||||
|
||||
private slots:
|
||||
void setup();
|
||||
|
||||
@ -86,6 +86,7 @@ QVariantList JsonTypes::s_configurationError;
|
||||
QVariantList JsonTypes::s_networkManagerError;
|
||||
QVariantList JsonTypes::s_networkManagerState;
|
||||
QVariantList JsonTypes::s_networkDeviceState;
|
||||
QVariantList JsonTypes::s_userError;
|
||||
|
||||
QVariantMap JsonTypes::s_paramType;
|
||||
QVariantMap JsonTypes::s_param;
|
||||
@ -141,6 +142,7 @@ void JsonTypes::init()
|
||||
s_networkManagerError = enumToStrings(NetworkManager::staticMetaObject, "NetworkManagerError");
|
||||
s_networkManagerState = enumToStrings(NetworkManager::staticMetaObject, "NetworkManagerState");
|
||||
s_networkDeviceState = enumToStrings(NetworkDevice::staticMetaObject, "NetworkDeviceState");
|
||||
s_userError = enumToStrings(UserManager::staticMetaObject, "UserError");
|
||||
|
||||
// ParamType
|
||||
s_paramType.insert("id", basicTypeToString(Uuid));
|
||||
@ -396,6 +398,7 @@ QVariantMap JsonTypes::allTypes()
|
||||
allTypes.insert("NetworkManagerError", networkManagerError());
|
||||
allTypes.insert("NetworkManagerState", networkManagerState());
|
||||
allTypes.insert("NetworkDeviceState", networkDeviceState());
|
||||
allTypes.insert("UserError", userError());
|
||||
|
||||
allTypes.insert("StateType", stateTypeDescription());
|
||||
allTypes.insert("StateDescriptor", stateDescriptorDescription());
|
||||
@ -1917,6 +1920,12 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
|
||||
qCWarning(dcJsonRpc) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(networkDeviceStateRef());
|
||||
return result;
|
||||
}
|
||||
} else if (refName == userErrorRef()) {
|
||||
QPair<bool, QString> result = validateEnum(s_userError, variant);
|
||||
if (!result.first) {
|
||||
qCWarning(dcJsonRpc) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(userErrorRef());
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data());
|
||||
return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName));
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
* *
|
||||
* Copyright (C) 2015 Simon Stürz <simon.stuerz@guh.io> *
|
||||
* Copyright (C) 2014 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* Copyright (C) 2017 Michael Zanetti <michael.zanetti@guh.io> *
|
||||
* *
|
||||
* This file is part of guh. *
|
||||
* *
|
||||
@ -28,6 +29,7 @@
|
||||
#include "devicemanager.h"
|
||||
#include "ruleengine.h"
|
||||
#include "guhconfiguration.h"
|
||||
#include "usermanager.h"
|
||||
|
||||
#include "types/event.h"
|
||||
#include "types/action.h"
|
||||
@ -132,6 +134,7 @@ public:
|
||||
DECLARE_TYPE(networkManagerError, "NetworkManagerError", NetworkManager, NetworkManagerError)
|
||||
DECLARE_TYPE(networkManagerState, "NetworkManagerState", NetworkManager, NetworkManagerState)
|
||||
DECLARE_TYPE(networkDeviceState, "NetworkDeviceState", NetworkDevice, NetworkDeviceState)
|
||||
DECLARE_TYPE(userError, "UserError", UserManager, UserError)
|
||||
|
||||
DECLARE_OBJECT(paramType, "ParamType")
|
||||
DECLARE_OBJECT(param, "Param")
|
||||
|
||||
@ -132,7 +132,7 @@ namespace guhserver {
|
||||
LogEngine::LogEngine(QObject *parent):
|
||||
QObject(parent)
|
||||
{
|
||||
m_db = QSqlDatabase::addDatabase("QSQLITE");
|
||||
m_db = QSqlDatabase::addDatabase("QSQLITE", "logs");
|
||||
m_db.setDatabaseName(GuhSettings::logPath());
|
||||
m_dbMaxSize = 20000;
|
||||
|
||||
@ -175,14 +175,14 @@ QList<LogEntry> LogEngine::logEntries(const LogFilter &filter) const
|
||||
|
||||
QString queryCall = "SELECT * FROM entries ORDER BY timestamp;";
|
||||
if (filter.isEmpty()) {
|
||||
query.exec(queryCall);
|
||||
query = m_db.exec(queryCall);
|
||||
} else {
|
||||
queryCall = QString("SELECT * FROM entries WHERE %1 ORDER BY timestamp;").arg(filter.queryString());
|
||||
query.exec(queryCall);
|
||||
query = m_db.exec(queryCall);
|
||||
}
|
||||
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcLogEngine) << "Error fetching log entries. Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.lastError().isValid()) {
|
||||
qCWarning(dcLogEngine) << "Error fetching log entries. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
return QList<LogEntry>();
|
||||
}
|
||||
|
||||
@ -209,10 +209,9 @@ void LogEngine::clearDatabase()
|
||||
{
|
||||
qCWarning(dcLogEngine) << "Clear logging database.";
|
||||
|
||||
QSqlQuery query;
|
||||
QString queryDeleteString = QString("DELETE FROM entries;");
|
||||
if (!query.exec(queryDeleteString)) {
|
||||
qCWarning(dcLogEngine) << "Could not clear logging database. Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.exec(queryDeleteString).lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcLogEngine) << "Could not clear logging database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
}
|
||||
|
||||
emit logDatabaseUpdated();
|
||||
@ -309,10 +308,9 @@ void LogEngine::removeDeviceLogs(const DeviceId &deviceId)
|
||||
{
|
||||
qCDebug(dcLogEngine) << "Deleting log entries from device" << deviceId.toString();
|
||||
|
||||
QSqlQuery query;
|
||||
QString queryDeleteString = QString("DELETE FROM entries WHERE deviceId = '%1';").arg(deviceId.toString());
|
||||
if (!query.exec(queryDeleteString)) {
|
||||
qCWarning(dcLogEngine) << "Error deleting log entries from device" << deviceId.toString() << ". Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.exec(queryDeleteString).lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcLogEngine) << "Error deleting log entries from device" << deviceId.toString() << ". Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
} else {
|
||||
emit logDatabaseUpdated();
|
||||
}
|
||||
@ -322,10 +320,9 @@ void LogEngine::removeRuleLogs(const RuleId &ruleId)
|
||||
{
|
||||
qCDebug(dcLogEngine) << "Deleting log entries from rule" << ruleId.toString();
|
||||
|
||||
QSqlQuery query;
|
||||
QString queryDeleteString = QString("DELETE FROM entries WHERE typeId = '%1';").arg(ruleId.toString());
|
||||
if (!query.exec(queryDeleteString)) {
|
||||
qCWarning(dcLogEngine) << "Error deleting log entries from rule" << ruleId.toString() << ". Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.exec(queryDeleteString).lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcLogEngine) << "Error deleting log entries from rule" << ruleId.toString() << ". Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
} else {
|
||||
emit logDatabaseUpdated();
|
||||
}
|
||||
@ -345,9 +342,8 @@ void LogEngine::appendLogEntry(const LogEntry &entry)
|
||||
.arg(entry.active())
|
||||
.arg(entry.errorCode());
|
||||
|
||||
QSqlQuery query;
|
||||
if (!query.exec(queryString)) {
|
||||
qCWarning(dcLogEngine) << "Error writing log entry. Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.exec(queryString).lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcLogEngine) << "Error writing log entry. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
qCWarning(dcLogEngine) << entry;
|
||||
return;
|
||||
}
|
||||
@ -358,8 +354,7 @@ void LogEngine::appendLogEntry(const LogEntry &entry)
|
||||
void LogEngine::checkDBSize()
|
||||
{
|
||||
QString queryString = "SELECT ROWID FROM entries;";
|
||||
QSqlQuery query;
|
||||
query.exec(queryString);
|
||||
QSqlQuery query = m_db.exec(queryString);
|
||||
int numRows = 0;
|
||||
if (m_db.driver()->hasFeature(QSqlDriver::QuerySize)) {
|
||||
numRows = query.size();
|
||||
@ -373,8 +368,8 @@ void LogEngine::checkDBSize()
|
||||
// keep only the latest m_dbMaxSize entries
|
||||
qCDebug(dcLogEngine) << "Deleting oldest entries and keep only the latest" << m_dbMaxSize << "entries.";
|
||||
QString queryDeleteString = QString("DELETE FROM entries WHERE ROWID IN (SELECT ROWID FROM entries ORDER BY timestamp DESC LIMIT -1 OFFSET %1);").arg(QString::number(m_dbMaxSize));
|
||||
if (!query.exec(queryDeleteString)) {
|
||||
qCWarning(dcLogEngine) << "Error deleting oldest log entries to keep size. Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.exec(queryDeleteString).lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcLogEngine) << "Error deleting oldest log entries to keep size. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
} else {
|
||||
emit logDatabaseUpdated();
|
||||
}
|
||||
@ -386,14 +381,13 @@ void LogEngine::initDB()
|
||||
m_db.close();
|
||||
m_db.open();
|
||||
|
||||
QSqlQuery query;
|
||||
|
||||
if (!m_db.tables().contains("metadata")) {
|
||||
query.exec("CREATE TABLE metadata (key varchar(10), data varchar(40));");
|
||||
query.exec(QString("INSERT INTO metadata (key, data) VALUES('version', '%1');").arg(DB_SCHEMA_VERSION));
|
||||
m_db.exec("CREATE TABLE metadata (key varchar(10), data varchar(40));");
|
||||
m_db.exec(QString("INSERT INTO metadata (key, data) VALUES('version', '%1');").arg(DB_SCHEMA_VERSION));
|
||||
}
|
||||
|
||||
query.exec("SELECT data FROM metadata WHERE key = 'version';");
|
||||
QSqlQuery query = m_db.exec("SELECT data FROM metadata WHERE key = 'version';");
|
||||
if (query.next()) {
|
||||
int version = query.value("data").toInt();
|
||||
if (version != DB_SCHEMA_VERSION) {
|
||||
@ -406,27 +400,27 @@ void LogEngine::initDB()
|
||||
}
|
||||
|
||||
if (!m_db.tables().contains("sourceTypes")) {
|
||||
query.exec("CREATE TABLE sourceTypes (id int, name varchar(20), PRIMARY KEY(id));");
|
||||
//qCDebug(dcLogEngine) << query.lastError().databaseText();
|
||||
m_db.exec("CREATE TABLE sourceTypes (id int, name varchar(20), PRIMARY KEY(id));");
|
||||
//qCDebug(dcLogEngine) << m_db.lastError().databaseText();
|
||||
QMetaEnum logTypes = Logging::staticMetaObject.enumerator(Logging::staticMetaObject.indexOfEnumerator("LoggingSource"));
|
||||
Q_ASSERT_X(logTypes.isValid(), "LogEngine", "Logging has no enum LoggingSource");
|
||||
for (int i = 0; i < logTypes.keyCount(); i++) {
|
||||
query.exec(QString("INSERT INTO sourceTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i)));
|
||||
m_db.exec(QString("INSERT INTO sourceTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_db.tables().contains("loggingEventTypes")) {
|
||||
query.exec("CREATE TABLE loggingEventTypes (id int, name varchar(20), PRIMARY KEY(id));");
|
||||
//qCDebug(dcLogEngine) << query.lastError().databaseText();
|
||||
m_db.exec("CREATE TABLE loggingEventTypes (id int, name varchar(20), PRIMARY KEY(id));");
|
||||
//qCDebug(dcLogEngine) << m_db.lastError().databaseText();
|
||||
QMetaEnum logTypes = Logging::staticMetaObject.enumerator(Logging::staticMetaObject.indexOfEnumerator("LoggingEventType"));
|
||||
Q_ASSERT_X(logTypes.isValid(), "LogEngine", "Logging has no enum LoggingEventType");
|
||||
for (int i = 0; i < logTypes.keyCount(); i++) {
|
||||
query.exec(QString("INSERT INTO loggingEventTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i)));
|
||||
m_db.exec(QString("INSERT INTO loggingEventTypes (id, name) VALUES(%1, '%2');").arg(i).arg(logTypes.key(i)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_db.tables().contains("entries")) {
|
||||
query.exec("CREATE TABLE entries "
|
||||
m_db.exec("CREATE TABLE entries "
|
||||
"("
|
||||
"timestamp int,"
|
||||
"loggingLevel int,"
|
||||
@ -441,8 +435,8 @@ void LogEngine::initDB()
|
||||
"FOREIGN KEY(loggingEventType) REFERENCES loggingEventTypes(id)"
|
||||
");");
|
||||
|
||||
if (query.lastError().isValid())
|
||||
qCWarning(dcLogEngine) << "Error creating log table in database. Driver error:" << query.lastError().driverText() << "Database error:" << query.lastError().databaseText();
|
||||
if (m_db.lastError().isValid())
|
||||
qCWarning(dcLogEngine) << "Error creating log table in database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -126,6 +126,7 @@ int main(int argc, char *argv[])
|
||||
s_loggingFilters.insert("Avahi", false);
|
||||
s_loggingFilters.insert("Cloud", true);
|
||||
s_loggingFilters.insert("NetworkManager", true);
|
||||
s_loggingFilters.insert("UserManager", true);
|
||||
|
||||
QHash<QString, bool> loggingFiltersPlugins;
|
||||
foreach (const QJsonObject &pluginMetadata, DeviceManager::pluginsMetadata()) {
|
||||
|
||||
@ -56,6 +56,7 @@ HEADERS += $$top_srcdir/server/guhcore.h \
|
||||
$$top_srcdir/server/networkmanager/networksettings.h \
|
||||
$$top_srcdir/server/networkmanager/networkconnection.h \
|
||||
$$top_srcdir/server/networkmanager/wirednetworkdevice.h \
|
||||
$$top_srcdir/server/usermanager.h \
|
||||
|
||||
|
||||
SOURCES += $$top_srcdir/server/guhcore.cpp \
|
||||
@ -111,4 +112,5 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \
|
||||
$$top_srcdir/server/networkmanager/networksettings.cpp \
|
||||
$$top_srcdir/server/networkmanager/networkconnection.cpp \
|
||||
$$top_srcdir/server/networkmanager/wirednetworkdevice.cpp \
|
||||
$$top_srcdir/server/usermanager.cpp \
|
||||
|
||||
|
||||
181
server/usermanager.cpp
Normal file
181
server/usermanager.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2017 Michael Zanetti <michael.zanetti@guh.io> *
|
||||
* *
|
||||
* This file is part of guh. *
|
||||
* *
|
||||
* Guh is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, version 2 of the License. *
|
||||
* *
|
||||
* Guh 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 guh. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "usermanager.h"
|
||||
#include "guhsettings.h"
|
||||
#include "loggingcategories.h"
|
||||
|
||||
#include <QUuid>
|
||||
#include <QCryptographicHash>
|
||||
#include <QSqlQuery>
|
||||
#include <QVariant>
|
||||
#include <QSqlError>
|
||||
#include <QRegExpValidator>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
UserManager::UserManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), "users");
|
||||
m_db.setDatabaseName(GuhSettings::settingsPath() + "/user-db.sqlite");
|
||||
|
||||
if (!m_db.open()) {
|
||||
qCWarning(dcUserManager) << "Error opening users database:" << m_db.lastError().driverText();
|
||||
return;
|
||||
}
|
||||
initDB();
|
||||
}
|
||||
|
||||
QStringList UserManager::users() const
|
||||
{
|
||||
QString userQuery("SELECT username FROM users;");
|
||||
QSqlQuery result = m_db.exec(userQuery);
|
||||
QStringList ret;
|
||||
while (result.next()) {
|
||||
ret << result.value("username").toString();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
UserManager::UserError UserManager::createUser(const QString &username, const QString &password)
|
||||
{
|
||||
if (!validateUsername(username)) {
|
||||
qCWarning(dcUserManager) << "Error creating user. Invalid username";
|
||||
return UserErrorInvalidUserId;
|
||||
}
|
||||
|
||||
QRegExp passwordValidator = QRegExp("^(?=.*[A-Za-z])(?=.*\[0-9])(?=.*[$@$!%*#?&])[A-Za-z0-9$@$!%*#?&]{8,}$");
|
||||
if (!passwordValidator.exactMatch(password)) {
|
||||
qCWarning(dcUserManager) << "Password failed character validation. Must contain a letter, a number and a special charactar. Minimum length: 8";
|
||||
return UserErrorBadPassword;
|
||||
}
|
||||
|
||||
QString checkForDuplicateUserQuery = QString("SELECT * FROM users WHERE username = \"%1\";").arg(username);
|
||||
QSqlQuery result = m_db.exec(checkForDuplicateUserQuery);
|
||||
if (result.first()) {
|
||||
qCWarning(dcUserManager) << "Username already in use";
|
||||
return UserErrorDuplicateUserId;
|
||||
}
|
||||
|
||||
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(QString::fromUtf8(hashedPassword))
|
||||
.arg(QString::fromUtf8(salt));
|
||||
m_db.exec(queryString);
|
||||
if (m_db.lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcUserManager) << "Error creating user:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
||||
return UserErrorBackendError;
|
||||
}
|
||||
return UserErrorNoError;
|
||||
}
|
||||
|
||||
UserManager::UserError UserManager::removeUser(const QString &username)
|
||||
{
|
||||
QString dropUserQuery = QString("DELETE FROM users WHERE username =\"%1\";").arg(username);
|
||||
QSqlQuery result = m_db.exec(dropUserQuery);
|
||||
if (result.numRowsAffected() == 0) {
|
||||
return UserErrorInvalidUserId;
|
||||
}
|
||||
|
||||
QString dropTokensQuery = QString("DELETE FROM tokens WHERE username = \"%1\";").arg(username);
|
||||
m_db.exec(dropTokensQuery);
|
||||
|
||||
return UserErrorNoError;
|
||||
}
|
||||
|
||||
QByteArray UserManager::authenticate(const QString &username, const QString &password, const QString &deviceName)
|
||||
{
|
||||
if (!validateUsername(username)) {
|
||||
qCWarning(dcUserManager) << "Username did not pass validation:" << username;
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QString passwordQuery = QString("SELECT password, salt FROM users WHERE username = \"%1\";").arg(username);
|
||||
QSqlQuery result = m_db.exec(passwordQuery);
|
||||
if (!result.first()) {
|
||||
qCWarning(dcUserManager) << "No such username" << username;
|
||||
return QByteArray();
|
||||
}
|
||||
QByteArray salt = result.value("salt").toByteArray();
|
||||
QByteArray hashedPassword = result.value("password").toByteArray();
|
||||
|
||||
if (hashedPassword != QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64()) {
|
||||
qCWarning(dcUserManager) << "Authentication error for user:" << username;
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
|
||||
QString storeTokenQuery = QString("INSERT INTO tokens(username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\");")
|
||||
.arg(username)
|
||||
.arg(QString::fromUtf8(token))
|
||||
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
|
||||
.arg(deviceName);
|
||||
|
||||
m_db.exec(storeTokenQuery);
|
||||
if (m_db.lastError().type() != QSqlError::NoError) {
|
||||
qCWarning(dcUserManager) << "Error storing token in DB:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
||||
return QByteArray();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
bool UserManager::verifyToken(const QByteArray &token)
|
||||
{
|
||||
QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)"));
|
||||
if (!validator.exactMatch(token)) {
|
||||
qCWarning(dcUserManager) << "Token failed character validation" << token;
|
||||
return false;
|
||||
}
|
||||
QString getTokenQuery = QString("SELECT * 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 false;
|
||||
}
|
||||
if (!result.first()) {
|
||||
qCDebug(dcUserManager) << "Authorisation failed for token" << token;
|
||||
return false;
|
||||
}
|
||||
qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UserManager::initDB()
|
||||
{
|
||||
if (!m_db.tables().contains("users")) {
|
||||
m_db.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE, password VARCHAR(100), salt VARCHAR(100));");
|
||||
}
|
||||
if (!m_db.tables().contains("tokens")) {
|
||||
m_db.exec("CREATE TABLE tokens (username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));");
|
||||
}
|
||||
}
|
||||
|
||||
bool UserManager::validateUsername(const QString &username) const
|
||||
{
|
||||
QRegExp validator("(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$)");
|
||||
return validator.exactMatch(username);
|
||||
}
|
||||
|
||||
}
|
||||
64
server/usermanager.h
Normal file
64
server/usermanager.h
Normal file
@ -0,0 +1,64 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2017 Michael Zanetti <michael.zanetti@guh.io> *
|
||||
* *
|
||||
* This file is part of guh. *
|
||||
* *
|
||||
* Guh is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, version 2 of the License. *
|
||||
* *
|
||||
* Guh 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 guh. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef USERMANAGER_H
|
||||
#define USERMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSqlDatabase>
|
||||
|
||||
namespace guhserver {
|
||||
|
||||
class UserManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum UserError {
|
||||
UserErrorNoError,
|
||||
UserErrorBackendError,
|
||||
UserErrorInvalidUserId,
|
||||
UserErrorDuplicateUserId,
|
||||
UserErrorBadPassword
|
||||
};
|
||||
Q_ENUM(UserError)
|
||||
|
||||
explicit UserManager(QObject *parent = 0);
|
||||
|
||||
QStringList users() const;
|
||||
|
||||
UserError createUser(const QString &username, const QString &password);
|
||||
UserError removeUser(const QString &username);
|
||||
|
||||
QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName);
|
||||
|
||||
bool verifyToken(const QByteArray &token);
|
||||
|
||||
private:
|
||||
void initDB();
|
||||
bool validateUsername(const QString &username) const;
|
||||
|
||||
private:
|
||||
QSqlDatabase m_db;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // USERMANAGER_H
|
||||
@ -372,6 +372,28 @@
|
||||
"o:eventType": "$ref:EventType"
|
||||
}
|
||||
},
|
||||
"JSONRPC.Authenticate": {
|
||||
"description": "Authenticate a client to the api. This will return a new token to be used to authorize a client at the API.",
|
||||
"params": {
|
||||
"deviceName": "String",
|
||||
"password": "String",
|
||||
"username": "String"
|
||||
},
|
||||
"returns": {
|
||||
"o:token": "String",
|
||||
"success": "String"
|
||||
}
|
||||
},
|
||||
"JSONRPC.CreateUser": {
|
||||
"description": "Create a new user in the API. Currently this is only allowed to be called once when a new guh 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"
|
||||
}
|
||||
},
|
||||
"JSONRPC.Introspect": {
|
||||
"description": "Introspect this API.",
|
||||
"params": {
|
||||
@ -1321,6 +1343,13 @@
|
||||
"UnitVoltAmpereReactive",
|
||||
"UnitAmpereHour"
|
||||
],
|
||||
"UserError": [
|
||||
"UserErrorNoError",
|
||||
"UserErrorBackendError",
|
||||
"UserErrorInvalidUserId",
|
||||
"UserErrorDuplicateUserId",
|
||||
"UserErrorBadPassword"
|
||||
],
|
||||
"ValueOperator": [
|
||||
"ValueOperatorEquals",
|
||||
"ValueOperatorNotEquals",
|
||||
|
||||
@ -117,6 +117,9 @@ GuhTestBase::GuhTestBase(QObject *parent) :
|
||||
m_mockDevice1Port = 1337 + (qrand() % 1000);
|
||||
m_mockDevice2Port = 7331 + (qrand() % 1000);
|
||||
QCoreApplication::instance()->setOrganizationName("guh-test");
|
||||
|
||||
GuhCore::instance()->userManager()->createUser("dummy@guh.io", "DummyPW1!");
|
||||
m_apiToken = GuhCore::instance()->userManager()->authenticate("dummy@guh.io", "DummyPW1!", "testcase");
|
||||
}
|
||||
|
||||
void GuhTestBase::initTestCase()
|
||||
@ -208,6 +211,7 @@ QVariant GuhTestBase::injectAndWait(const QString &method, const QVariantMap &pa
|
||||
call.insert("id", m_commandId);
|
||||
call.insert("method", method);
|
||||
call.insert("params", params);
|
||||
call.insert("token", m_apiToken);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
|
||||
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
||||
|
||||
@ -188,6 +188,7 @@ protected:
|
||||
int m_mockDevice2Port;
|
||||
|
||||
DeviceId m_mockDeviceId;
|
||||
QByteArray m_apiToken;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -42,6 +42,8 @@ class TestJSONRPC: public GuhTestBase
|
||||
private slots:
|
||||
void testHandshake();
|
||||
|
||||
void testInitialSetup();
|
||||
|
||||
void testBasicCall_data();
|
||||
void testBasicCall();
|
||||
|
||||
@ -107,6 +109,152 @@ void TestJSONRPC::testHandshake()
|
||||
m_mockTcpServer->clientDisconnected(newClientId);
|
||||
}
|
||||
|
||||
void TestJSONRPC::testInitialSetup()
|
||||
{
|
||||
foreach (const QString &user, GuhCore::instance()->userManager()->users()) {
|
||||
GuhCore::instance()->userManager()->removeUser(user);
|
||||
}
|
||||
QCOMPARE(GuhCore::instance()->userManager()->users().count(), 0);
|
||||
|
||||
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
|
||||
QVERIFY(spy.isValid());
|
||||
|
||||
// Introspect call should work in any case
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Introspect\"}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
QVariantMap response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling introspect on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
|
||||
|
||||
// Any other call should fail with "unauthorized" even if we use a previously valid token
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Version on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized"));
|
||||
|
||||
// Except CreateUser
|
||||
|
||||
// But it should still fail when giving a an invalid username
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\"}}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling CreateUser on uninitialized instance with invalid user:" << response.value("status").toString() << response.value("params").toMap().value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(GuhCore::instance()->userManager()->users().count(), 0);
|
||||
|
||||
// or when giving a bad password
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"weak\"}}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling CreateUser on uninitialized instance with weak password:" << response.value("status").toString() << response.value("params").toMap().value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(GuhCore::instance()->userManager()->users().count(), 0);
|
||||
|
||||
// Now lets play by the rules
|
||||
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().toMap();
|
||||
qWarning() << "Calling CreateUser on uninitialized instance:" << response.value("status").toString() << response.value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(GuhCore::instance()->userManager()->users().count(), 1);
|
||||
|
||||
// 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\"}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Version with old token:" << response.value("status").toString() << response.value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized"));
|
||||
|
||||
// Now lets authenticate with a wrong user
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@wrong.domain\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Authenticate with wrong user:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(response.value("params").toMap().value("success").toBool(), false);
|
||||
QVERIFY(response.value("params").toMap().value("token").toByteArray().isEmpty());
|
||||
|
||||
|
||||
// Now lets authenticate with a wrong password
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Authenticate with wrong password:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(response.value("params").toMap().value("success").toBool(), false);
|
||||
QVERIFY(response.value("params").toMap().value("token").toByteArray().isEmpty());
|
||||
|
||||
|
||||
// Now lets authenticate for real
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Authenticate with valid credentials:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
QCOMPARE(response.value("params").toMap().value("success").toBool(), true);
|
||||
m_apiToken = response.value("params").toMap().value("token").toByteArray();
|
||||
QVERIFY(!m_apiToken.isEmpty());
|
||||
|
||||
// Now do a Version call with the valid token and it should work
|
||||
spy.clear();
|
||||
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}");
|
||||
if (spy.count() == 0) {
|
||||
spy.wait();
|
||||
}
|
||||
QVERIFY(spy.count() == 1);
|
||||
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
|
||||
response = jsonDoc.toVariant().toMap();
|
||||
qWarning() << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString();
|
||||
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
|
||||
|
||||
}
|
||||
|
||||
void TestJSONRPC::testBasicCall_data()
|
||||
{
|
||||
QTest::addColumn<QByteArray>("call");
|
||||
|
||||
7
tests/scripts/authenticate.sh
Executable file
7
tests/scripts/authenticate.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z $4 ]; then
|
||||
echo "usage: $0 host username password devicename"
|
||||
else
|
||||
(echo '{"id":1, "method": "JSONRPC.Authenticate", "params": { "username": "'$2'", "password": "'$3'", "deviceName": "'$4'"}}'; sleep 1) | nc $1 2222
|
||||
fi
|
||||
7
tests/scripts/createuser.sh
Executable file
7
tests/scripts/createuser.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z $3 ]; then
|
||||
echo "usage: $0 host username password"
|
||||
else
|
||||
(echo '{"id":1, "method": "JSONRPC.CreateUser", "params": { "username": "'$2'", "password": "'$3'"}}'; sleep 1) | nc $1 2222
|
||||
fi
|
||||
@ -3,5 +3,5 @@
|
||||
if [ -z $1 ]; then
|
||||
echo "usage: $0 host"
|
||||
else
|
||||
(echo '{"id":1, "method":"Devices.GetConfiguredDevices"}'; sleep 1) | nc $1 2222
|
||||
(echo '{"id":1, "token": "'$2'", "method":"Devices.GetConfiguredDevices"}'; sleep 1) | nc $1 2222
|
||||
fi
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="149"/>
|
||||
<location filename="../server/main.cpp" line="150"/>
|
||||
<source>
|
||||
guh ( /[guːh]/ ) is an open source IoT (Internet of Things) server,
|
||||
which allows to control a lot of different devices from many different
|
||||
@ -23,12 +23,12 @@ Szenen undVerhaltensweisen des Systems festzulegen.
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="161"/>
|
||||
<location filename="../server/main.cpp" line="162"/>
|
||||
<source>Run guhd in the foreground, not as daemon.</source>
|
||||
<translation>Starte guhd im Vordergrund, nicht als Service.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="164"/>
|
||||
<location filename="../server/main.cpp" line="165"/>
|
||||
<source>Debug categories to enable. Prefix with "No" to disable. Warnings from all categories will be printed unless explicitly muted with "NoWarnings".
|
||||
|
||||
Categories are:</source>
|
||||
@ -36,17 +36,17 @@ Categories are:</source>
|
||||
Es gibt folgende Kategorien:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="181"/>
|
||||
<location filename="../server/main.cpp" line="182"/>
|
||||
<source>Enables all debug categories. This parameter overrides all debug category parameters.</source>
|
||||
<translation>Aktiviere alle Debug-Kategorien. Dieser Parameter überschreibt alle anderen Debug-Kategorien Parameter.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="186"/>
|
||||
<location filename="../server/main.cpp" line="187"/>
|
||||
<source>Specify a log file to write to, If this option is not specified, logs will be printed to the standard output.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="218"/>
|
||||
<location filename="../server/main.cpp" line="219"/>
|
||||
<source>No such debug category:</source>
|
||||
<translation>Diese Debug-Kategorie existiert nicht:</translation>
|
||||
</message>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="149"/>
|
||||
<location filename="../server/main.cpp" line="150"/>
|
||||
<source>
|
||||
guh ( /[guːh]/ ) is an open source IoT (Internet of Things) server,
|
||||
which allows to control a lot of different devices from many different
|
||||
@ -23,12 +23,12 @@ for your environment.
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="161"/>
|
||||
<location filename="../server/main.cpp" line="162"/>
|
||||
<source>Run guhd in the foreground, not as daemon.</source>
|
||||
<translation>Run guhd in the foreground, not as daemon.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="164"/>
|
||||
<location filename="../server/main.cpp" line="165"/>
|
||||
<source>Debug categories to enable. Prefix with "No" to disable. Warnings from all categories will be printed unless explicitly muted with "NoWarnings".
|
||||
|
||||
Categories are:</source>
|
||||
@ -37,17 +37,17 @@ Categories are:</source>
|
||||
Categories are:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="181"/>
|
||||
<location filename="../server/main.cpp" line="182"/>
|
||||
<source>Enables all debug categories. This parameter overrides all debug category parameters.</source>
|
||||
<translation>Enables all debug categories. This parameter overrides all debug category parameters.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="186"/>
|
||||
<location filename="../server/main.cpp" line="187"/>
|
||||
<source>Specify a log file to write to, If this option is not specified, logs will be printed to the standard output.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../server/main.cpp" line="218"/>
|
||||
<location filename="../server/main.cpp" line="219"/>
|
||||
<source>No such debug category:</source>
|
||||
<translation>No such debug category:</translation>
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user