add methods to revoke existing tokens again

This commit is contained in:
Michael Zanetti 2017-08-08 18:40:58 +02:00
parent 08727a07ba
commit 53cca56fd3
12 changed files with 383 additions and 7 deletions

View File

@ -113,6 +113,19 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject
returns.insert("o:token", JsonTypes::basicTypeToString(JsonTypes::String));
setReturns("Authenticate", returns);
params.clear(); returns.clear();
setDescription("Tokens", "Return a list of if TokenInfo objects all the tokens for the current user.");
setParams("Tokens", params);
returns.insert("tokenInfoList", QVariantList() << JsonTypes::tokenInfoRef());
setReturns("Tokens", returns);
params.clear(); returns.clear();
setDescription("RemoveToken", "Revoke access for a given token.");
params.insert("tokenId", JsonTypes::basicTypeToString(JsonTypes::Uuid));
setParams("RemoveToken", params);
returns.insert("error", JsonTypes::userErrorRef());
setReturns("RemoveToken", returns);
QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection);
}
@ -189,6 +202,35 @@ JsonReply *JsonRPCServer::Authenticate(const QVariantMap &params)
return createReply(ret);
}
JsonReply *JsonRPCServer::Tokens(const QVariantMap &params) const
{
Q_UNUSED(params)
QByteArray token = property("token").toByteArray();
QString username = GuhCore::instance()->userManager()->userForToken(token);
if (username.isEmpty()) {
// There *really* should be a user for the token in the DB
Q_ASSERT(false);
}
QList<TokenInfo> tokens = GuhCore::instance()->userManager()->tokens(username);
QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) {
retList << JsonTypes::packTokenInfo(tokenInfo);
}
QVariantMap retMap;
retMap.insert("tokenInfoList", retList);
return createReply(retMap);
}
JsonReply *JsonRPCServer::RemoveToken(const QVariantMap &params)
{
QUuid tokenId = params.value("tokenId").toUuid();
UserManager::UserError error = GuhCore::instance()->userManager()->removeToken(tokenId);
QVariantMap ret;
ret.insert("error", JsonTypes::userErrorToString(error));
return createReply(ret);
}
/*! Returns the list of registred \l{JsonHandler}{JsonHandlers} and their name.*/
QHash<QString, JsonHandler *> JsonRPCServer::handlers() const
{
@ -324,6 +366,7 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data)
// Hack: attach clientId to handler to be able to handle the JSONRPC methods. Do not use this outside of jsonrpcserver
handler->setProperty("clientId", clientId);
handler->setProperty("token", message.value("token").toByteArray());
JsonReply *reply;
QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params));

View File

@ -53,6 +53,8 @@ public:
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params);
Q_INVOKABLE JsonReply *Tokens(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params);
QHash<QString, JsonHandler *> handlers() const;

View File

@ -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. *
* *
@ -117,6 +118,7 @@ QVariantMap JsonTypes::s_repeatingOption;
QVariantMap JsonTypes::s_wirelessAccessPoint;
QVariantMap JsonTypes::s_wiredNetworkDevice;
QVariantMap JsonTypes::s_wirelessNetworkDevice;
QVariantMap JsonTypes::s_tokenInfo;
void JsonTypes::init()
{
@ -351,6 +353,12 @@ void JsonTypes::init()
s_wirelessNetworkDevice.insert("bitRate", basicTypeToString(QVariant::String));
s_wirelessNetworkDevice.insert("o:currentAccessPoint", wirelessAccessPointRef());
// TokenInfo
s_tokenInfo.insert("id", basicTypeToString(QVariant::Uuid));
s_tokenInfo.insert("userName", basicTypeToString(QVariant::String));
s_tokenInfo.insert("deviceName", basicTypeToString(QVariant::String));
s_tokenInfo.insert("creationTime", basicTypeToString(QVariant::UInt));
s_initialized = true;
}
@ -428,6 +436,7 @@ QVariantMap JsonTypes::allTypes()
allTypes.insert("WirelessAccessPoint", wirelessAccessPointDescription());
allTypes.insert("WiredNetworkDevice", wiredNetworkDeviceDescription());
allTypes.insert("WirelessNetworkDevice", wirelessNetworkDeviceDescription());
allTypes.insert("TokenInfo", tokenInfoDescription());
return allTypes;
}
@ -1167,6 +1176,16 @@ QVariantList JsonTypes::packPlugins()
return pluginsList;
}
QVariantMap JsonTypes::packTokenInfo(const TokenInfo &tokenInfo)
{
QVariantMap ret;
ret.insert("id", tokenInfo.id().toString());
ret.insert("userName", tokenInfo.username());
ret.insert("deviceName", tokenInfo.deviceName());
ret.insert("creationTime", tokenInfo.creationTime().toTime_t());
return ret;
}
/*! Returns the type string for the given \a type. */
QString JsonTypes::basicTypeToString(const QVariant::Type &type)
{
@ -1794,6 +1813,12 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
qCWarning(dcJsonRpc) << "WirelessNetworkDevice not matching";
return result;
}
} else if (refName == tokenInfoRef()) {
QPair<bool, QString> result = validateMap(tokenInfoDescription(), variant.toMap());
if (!result.first) {
qCWarning(dcJsonRpc) << "TokenInfo not matching";
return result;
}
} else if (refName == basicTypeRef()) {
QPair<bool, QString> result = validateBasicType(variant);
if (!result.first) {

View File

@ -165,6 +165,7 @@ public:
DECLARE_OBJECT(wirelessAccessPoint, "WirelessAccessPoint")
DECLARE_OBJECT(wiredNetworkDevice, "WiredNetworkDevice")
DECLARE_OBJECT(wirelessNetworkDevice, "WirelessNetworkDevice")
DECLARE_OBJECT(tokenInfo, "TokenInfo")
// pack types
static QVariantMap packEventType(const EventType &eventType);
@ -197,7 +198,6 @@ public:
static QVariantMap packWiredNetworkDevice(WiredNetworkDevice *networkDevice);
static QVariantMap packWirelessNetworkDevice(WirelessNetworkDevice *networkDevice);
// pack resources
static QVariantList packRules(const QList<Rule> rules);
static QVariantList packCreateMethods(DeviceClass::CreateMethods createMethods);
static QVariantList packSupportedVendors();
@ -219,6 +219,8 @@ public:
static QVariantList packEventTypes(const DeviceClass &deviceClass);
static QVariantList packPlugins();
static QVariantMap packTokenInfo(const TokenInfo &tokenInfo);
static QString basicTypeToString(const QVariant::Type &type);
// unpack Types

View File

@ -57,6 +57,7 @@ HEADERS += $$top_srcdir/server/guhcore.h \
$$top_srcdir/server/networkmanager/networkconnection.h \
$$top_srcdir/server/networkmanager/wirednetworkdevice.h \
$$top_srcdir/server/usermanager.h \
$$PWD/tokeninfo.h
SOURCES += $$top_srcdir/server/guhcore.cpp \
@ -113,4 +114,5 @@ SOURCES += $$top_srcdir/server/guhcore.cpp \
$$top_srcdir/server/networkmanager/networkconnection.cpp \
$$top_srcdir/server/networkmanager/wirednetworkdevice.cpp \
$$top_srcdir/server/usermanager.cpp \
$$PWD/tokeninfo.cpp

54
server/tokeninfo.cpp Normal file
View File

@ -0,0 +1,54 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 "tokeninfo.h"
namespace guhserver {
TokenInfo::TokenInfo(const QUuid &id, const QString &username, const QDateTime &creationTime, const QString &deviceName):
m_id(id),
m_username(username),
m_creationTime(creationTime),
m_deviceName(deviceName)
{
}
QUuid TokenInfo::id() const
{
return m_id;
}
QString TokenInfo::username() const
{
return m_username;
}
QDateTime TokenInfo::creationTime() const
{
return m_creationTime;
}
QString TokenInfo::deviceName() const
{
return m_deviceName;
}
}

48
server/tokeninfo.h Normal file
View File

@ -0,0 +1,48 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 TOKENINFO_H
#define TOKENINFO_H
#include <QUuid>
#include <QDateTime>
namespace guhserver {
class TokenInfo
{
public:
TokenInfo(const QUuid &id, const QString &username, const QDateTime &creationTime, const QString &deviceName);
QUuid id() const;
QString username() const;
QDateTime creationTime() const;
QString deviceName() const;
private:
QUuid m_id;
QString m_username;
QDateTime m_creationTime;
QString m_deviceName;
};
}
#endif // TOKENINFO_H

View File

@ -126,7 +126,8 @@ 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(username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\");")
QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
.arg(QUuid::createUuid().toString())
.arg(username)
.arg(QString::fromUtf8(token))
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
@ -140,10 +141,70 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
return token;
}
QString UserManager::userForToken(const QByteArray &token) const
{
if (!validateToken(token)) {
qCWarning(dcUserManager) << "Token failed character validation:" << token;
return QString();
}
QString getUserQuery = QString("SELECT * FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
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();
}
return result.value("username").toString();
}
QList<TokenInfo> UserManager::tokens(const QString &username) const
{
QList<TokenInfo> ret;
if (!validateUsername(username)) {
qCWarning(dcUserManager) << "Username did not pass validation:" << username;
return ret;
}
QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE username = \"%1\";")
.arg(username);
QSqlQuery result = m_db.exec(getTokensQuery);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery;
return ret;
}
while (result.next()) {
ret << TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString());
}
return ret;
}
UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
{
QString removeTokenQuery = QString("DELETE FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
QSqlQuery result = m_db.exec(removeTokenQuery);
if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Removing token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << removeTokenQuery;
return UserErrorBackendError;
}
if (result.numRowsAffected() != 1) {
qCWarning(dcUserManager) << "Token not found in DB";
return UserErrorTokenNotFound;
}
qCDebug(dcUserManager) << "Token" << tokenId << "removed from DB";
return UserErrorNoError;
}
bool UserManager::verifyToken(const QByteArray &token)
{
QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)"));
if (!validator.exactMatch(token)) {
if (!validateToken(token)) {
qCWarning(dcUserManager) << "Token failed character validation" << token;
return false;
}
@ -168,7 +229,7 @@ void UserManager::initDB()
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));");
m_db.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));");
}
}
@ -178,4 +239,10 @@ bool UserManager::validateUsername(const QString &username) const
return validator.exactMatch(username);
}
bool UserManager::validateToken(const QByteArray &token) const
{
QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)"));
return validator.exactMatch(token);
}
}

View File

@ -21,6 +21,8 @@
#ifndef USERMANAGER_H
#define USERMANAGER_H
#include "tokeninfo.h"
#include <QObject>
#include <QSqlDatabase>
@ -35,7 +37,9 @@ public:
UserErrorBackendError,
UserErrorInvalidUserId,
UserErrorDuplicateUserId,
UserErrorBadPassword
UserErrorBadPassword,
UserErrorTokenNotFound,
UserErrorPermissionDenied
};
Q_ENUM(UserError)
@ -47,12 +51,16 @@ public:
UserError removeUser(const QString &username);
QByteArray authenticate(const QString &username, const QString &password, const QString &deviceName);
QString userForToken(const QByteArray &token) const;
QList<TokenInfo> tokens(const QString &username) const;
guhserver::UserManager::UserError removeToken(const QUuid &tokenId);
bool verifyToken(const QByteArray &token);
private:
void initDB();
bool validateUsername(const QString &username) const;
bool validateToken(const QByteArray &token) const;
private:
QSqlDatabase m_db;

View File

@ -403,6 +403,15 @@
"types": "Object"
}
},
"JSONRPC.RemoveToken": {
"description": "Revoke access for a given token.",
"params": {
"tokenId": "Uuid"
},
"returns": {
"error": "$ref:UserError"
}
},
"JSONRPC.SetNotificationStatus": {
"description": "Enable/Disable notifications for this connections.",
"params": {
@ -412,6 +421,16 @@
"enabled": "Bool"
}
},
"JSONRPC.Tokens": {
"description": "Return a list of if TokenInfo objects all the tokens for the current user.",
"params": {
},
"returns": {
"tokenInfoList": [
"$ref:TokenInfo"
]
}
},
"JSONRPC.Version": {
"description": "Version of this Guh/JSONRPC interface.",
"params": {
@ -1291,6 +1310,12 @@
"o:repeating": "$ref:RepeatingOption",
"o:time": "Time"
},
"TokenInfo": {
"creationTime": "Uint",
"deviceName": "String",
"id": "Uuid",
"userName": "String"
},
"Unit": [
"UnitNone",
"UnitSeconds",
@ -1348,7 +1373,9 @@
"UserErrorBackendError",
"UserErrorInvalidUserId",
"UserErrorDuplicateUserId",
"UserErrorBadPassword"
"UserErrorBadPassword",
"UserErrorTokenNotFound",
"UserErrorPermissionDenied"
],
"ValueOperator": [
"ValueOperatorEquals",

View File

@ -118,6 +118,7 @@ GuhTestBase::GuhTestBase(QObject *parent) :
m_mockDevice2Port = 7331 + (qrand() % 1000);
QCoreApplication::instance()->setOrganizationName("guh-test");
GuhCore::instance()->userManager()->removeUser("dummy@guh.io");
GuhCore::instance()->userManager()->createUser("dummy@guh.io", "DummyPW1!");
m_apiToken = GuhCore::instance()->userManager()->authenticate("dummy@guh.io", "DummyPW1!", "testcase");
}

View File

@ -44,6 +44,8 @@ private slots:
void testInitialSetup();
void testRevokeToken();
void testBasicCall_data();
void testBasicCall();
@ -255,6 +257,101 @@ void TestJSONRPC::testInitialSetup()
}
void TestJSONRPC::testRevokeToken()
{
QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
QVERIFY(spy.isValid());
// Now get all the tokens
spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}");
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() << "Getting existing Tokens" << response.value("status").toString() << response;
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
QVariantList tokenList = response.value("params").toMap().value("tokenInfoList").toList();
QCOMPARE(tokenList.count(), 1);
QUuid oldTokenId = tokenList.first().toMap().value("id").toUuid();
// Authenticate and create a new token
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);
QByteArray newToken = response.value("params").toMap().value("token").toByteArray();
QVERIFY(!newToken.isEmpty());
// Now do a Version call with the new token and it should work
spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"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"));
// Now get all the tokens using the old token
spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}");
if (spy.count() == 0) {
spy.wait();
}
QVERIFY(spy.count() == 1);
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
response = jsonDoc.toVariant().toMap();
qWarning() << "Calling Tokens" << response.value("status").toString();
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
tokenList = response.value("params").toMap().value("tokenInfoList").toList();
QCOMPARE(tokenList.count(), 2);
// find the new token
QUuid newTokenId;
foreach (const QVariant &tokenInfo, tokenList) {
if (tokenInfo.toMap().value("id").toUuid() != oldTokenId) {
newTokenId = tokenInfo.toMap().value("id").toUuid();
break;
}
}
// Revoke the new token
spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}");
if (spy.count() == 0) {
spy.wait();
}
QVERIFY(spy.count() == 1);
jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray());
response = jsonDoc.toVariant().toMap();
qWarning() << "Calling RemoveToken" << response.value("status").toString() << response;
QCOMPARE(response.value("status").toString(), QStringLiteral("success"));
// Do a call with the now removed token, it should be forbidden
spy.clear();
m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"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("unauthorized"));
}
void TestJSONRPC::testBasicCall_data()
{
QTest::addColumn<QByteArray>("call");