From fb9417892041d868dfab5251bdf98fd104bfade5 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 8 Feb 2020 01:30:11 +0100 Subject: [PATCH] Move authentication to Users namespace --- libnymea-core/jsonrpc/actionhandler.cpp | 8 +- libnymea-core/jsonrpc/actionhandler.h | 4 +- libnymea-core/jsonrpc/devicehandler.cpp | 69 +++-- libnymea-core/jsonrpc/devicehandler.h | 28 +-- libnymea-core/jsonrpc/eventhandler.cpp | 5 +- libnymea-core/jsonrpc/eventhandler.h | 2 +- .../jsonrpc/jsonrpcserverimplementation.cpp | 119 +++++---- .../jsonrpc/jsonrpcserverimplementation.h | 11 +- libnymea-core/jsonrpc/statehandler.cpp | 5 +- libnymea-core/jsonrpc/statehandler.h | 2 +- libnymea-core/jsonrpc/usershandler.cpp | 114 ++++++++- libnymea-core/jsonrpc/usershandler.h | 45 +++- libnymea/jsonrpc/jsoncontext.cpp | 58 +++++ libnymea/jsonrpc/jsoncontext.h | 54 ++++ libnymea/jsonrpc/jsonhandler.h | 2 +- libnymea/libnymea.pro | 2 + tests/auto/jsonrpc/testjsonrpc.cpp | 12 +- tests/auto/usermanager/testusermanager.cpp | 237 +++++++++++++++++- tests/auto/usermanager/usermanager.pro | 7 +- 19 files changed, 640 insertions(+), 144 deletions(-) create mode 100644 libnymea/jsonrpc/jsoncontext.cpp create mode 100644 libnymea/jsonrpc/jsoncontext.h diff --git a/libnymea-core/jsonrpc/actionhandler.cpp b/libnymea-core/jsonrpc/actionhandler.cpp index 993f3710..6c4623fc 100644 --- a/libnymea-core/jsonrpc/actionhandler.cpp +++ b/libnymea-core/jsonrpc/actionhandler.cpp @@ -109,12 +109,12 @@ QString ActionHandler::name() const return "Actions"; } -JsonReply* ActionHandler::ExecuteAction(const QVariantMap ¶ms) +JsonReply* ActionHandler::ExecuteAction(const QVariantMap ¶ms, const JsonContext &context) { DeviceId deviceId(params.value("deviceId").toString()); ActionTypeId actionTypeId(params.value("actionTypeId").toString()); ParamList actionParams = unpack(params.value("params")); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); Action action(actionTypeId, deviceId); action.setParams(actionParams); @@ -135,9 +135,9 @@ JsonReply* ActionHandler::ExecuteAction(const QVariantMap ¶ms) return jsonReply; } -JsonReply *ActionHandler::GetActionType(const QVariantMap ¶ms) const +JsonReply *ActionHandler::GetActionType(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); qCDebug(dcJsonRpc) << "asked for action type" << params; ActionTypeId actionTypeId(params.value("actionTypeId").toString()); foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) { diff --git a/libnymea-core/jsonrpc/actionhandler.h b/libnymea-core/jsonrpc/actionhandler.h index ded30546..d76b26ba 100644 --- a/libnymea-core/jsonrpc/actionhandler.h +++ b/libnymea-core/jsonrpc/actionhandler.h @@ -44,8 +44,8 @@ public: QString name() const; - Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *GetActionType(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *GetActionType(const QVariantMap ¶ms, const JsonContext &context) const; Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap ¶ms); diff --git a/libnymea-core/jsonrpc/devicehandler.cpp b/libnymea-core/jsonrpc/devicehandler.cpp index ad496a68..b8488f66 100644 --- a/libnymea-core/jsonrpc/devicehandler.cpp +++ b/libnymea-core/jsonrpc/devicehandler.cpp @@ -415,13 +415,12 @@ QString DeviceHandler::name() const return "Devices"; } -JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); - + Q_UNUSED(params) QVariantList vendors; foreach (const Vendor &vendor, NymeaCore::instance()->deviceManager()->supportedVendors()) { - Vendor translatedVendor = NymeaCore::instance()->deviceManager()->translateVendor(vendor, locale); + Vendor translatedVendor = NymeaCore::instance()->deviceManager()->translateVendor(vendor, context.locale()); vendors.append(pack(translatedVendor)); } @@ -430,14 +429,13 @@ JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap ¶ms) const return createReply(returns); } -JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); VendorId vendorId = VendorId(params.value("vendorId").toString()); QVariantMap returns; QVariantList deviceClasses; foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices(vendorId)) { - DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale); + DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale()); deviceClasses.append(pack(translatedDeviceClass)); } @@ -445,9 +443,9 @@ JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap ¶ms) const return createReply(returns); } -JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap ¶ms) const +JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); QVariantMap returns; @@ -479,14 +477,13 @@ JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap ¶ms) const return reply; } -JsonReply* DeviceHandler::GetPlugins(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetPlugins(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); - + Q_UNUSED(params) QVariantList plugins; foreach (DevicePlugin* plugin, NymeaCore::instance()->deviceManager()->plugins()) { QVariantMap packedPlugin = pack(*plugin).toMap(); - packedPlugin["displayName"] = NymeaCore::instance()->deviceManager()->translate(plugin->pluginId(), plugin->pluginDisplayName(), locale); + packedPlugin["displayName"] = NymeaCore::instance()->deviceManager()->translate(plugin->pluginId(), plugin->pluginDisplayName(), context.locale()); plugins.append(packedPlugin); } @@ -524,13 +521,13 @@ JsonReply* DeviceHandler::SetPluginConfiguration(const QVariantMap ¶ms) return createReply(returns); } -JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap ¶ms) +JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap ¶ms, const JsonContext &context) { DeviceClassId deviceClassId(params.value("deviceClassId").toString()); QString deviceName = params.value("name").toString(); ParamList deviceParams = unpack(params.value("deviceParams")); DeviceDescriptorId deviceDescriptorId(params.value("deviceDescriptorId").toString()); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); JsonReply *jsonReply = createAsyncReply("AddConfiguredDevice"); @@ -558,11 +555,11 @@ JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap ¶ms) return jsonReply; } -JsonReply *DeviceHandler::PairDevice(const QVariantMap ¶ms) +JsonReply *DeviceHandler::PairDevice(const QVariantMap ¶ms, const JsonContext &context) { QString deviceName = params.value("name").toString(); ParamList deviceParams = unpack(params.value("deviceParams")); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); DevicePairingInfo *info; if (params.contains("deviceDescriptorId")) { @@ -603,12 +600,12 @@ JsonReply *DeviceHandler::PairDevice(const QVariantMap ¶ms) return jsonReply; } -JsonReply *DeviceHandler::ConfirmPairing(const QVariantMap ¶ms) +JsonReply *DeviceHandler::ConfirmPairing(const QVariantMap ¶ms, const JsonContext &context) { PairingTransactionId pairingTransactionId = PairingTransactionId(params.value("pairingTransactionId").toString()); QString secret = params.value("secret").toString(); QString username = params.value("username").toString(); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); JsonReply *jsonReply = createAsyncReply("ConfirmPairing"); @@ -651,12 +648,12 @@ JsonReply* DeviceHandler::GetConfiguredDevices(const QVariantMap ¶ms) const return createReply(returns); } -JsonReply *DeviceHandler::ReconfigureDevice(const QVariantMap ¶ms) +JsonReply *DeviceHandler::ReconfigureDevice(const QVariantMap ¶ms, const JsonContext &context) { DeviceId deviceId = DeviceId(params.value("deviceId").toString()); ParamList deviceParams = unpack(params.value("deviceParams")); DeviceDescriptorId deviceDescriptorId(params.value("deviceDescriptorId").toString()); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); JsonReply *jsonReply = createAsyncReply("ReconfigureDevice"); @@ -739,36 +736,30 @@ JsonReply *DeviceHandler::SetDeviceSettings(const QVariantMap ¶ms) return createReply(statusToReply(status)); } -JsonReply* DeviceHandler::GetEventTypes(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetEventTypes(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); - DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString())); - DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale); + DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale()); QVariantMap returns; returns.insert("eventTypes", pack(translatedDeviceClass.eventTypes())); return createReply(returns); } -JsonReply* DeviceHandler::GetActionTypes(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetActionTypes(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); - DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString())); - DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale); + DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale()); QVariantMap returns; returns.insert("actionTypes", pack(translatedDeviceClass.actionTypes())); return createReply(returns); } -JsonReply* DeviceHandler::GetStateTypes(const QVariantMap ¶ms) const +JsonReply* DeviceHandler::GetStateTypes(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); - DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString())); - DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale); + DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale()); QVariantMap returns; returns.insert("stateTypes", pack(translatedDeviceClass.stateTypes())); @@ -803,14 +794,14 @@ JsonReply *DeviceHandler::GetStateValues(const QVariantMap ¶ms) const return createReply(returns); } -JsonReply *DeviceHandler::BrowseDevice(const QVariantMap ¶ms) const +JsonReply *DeviceHandler::BrowseDevice(const QVariantMap ¶ms, const JsonContext &context) const { DeviceId deviceId = DeviceId(params.value("deviceId").toString()); QString itemId = params.value("itemId").toString(); JsonReply *jsonReply = createAsyncReply("BrowseDevice"); - BrowseResult *result = NymeaCore::instance()->deviceManager()->browseDevice(deviceId, itemId, params.value("locale").toLocale()); + BrowseResult *result = NymeaCore::instance()->deviceManager()->browseDevice(deviceId, itemId, context.locale()); connect(result, &BrowseResult::finished, jsonReply, [this, jsonReply, result](){ QVariantMap returns = statusToReply(result->status()); @@ -826,7 +817,7 @@ JsonReply *DeviceHandler::BrowseDevice(const QVariantMap ¶ms) const return jsonReply; } -JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap ¶ms) const +JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap ¶ms, const JsonContext &context) const { QVariantMap returns; DeviceId deviceId = DeviceId(params.value("deviceId").toString()); @@ -834,7 +825,7 @@ JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap ¶ms) const JsonReply *jsonReply = createAsyncReply("GetBrowserItem"); - BrowserItemResult *result = NymeaCore::instance()->deviceManager()->browserItemDetails(deviceId, itemId, params.value("locale").toLocale()); + BrowserItemResult *result = NymeaCore::instance()->deviceManager()->browserItemDetails(deviceId, itemId, context.locale()); connect(result, &BrowserItemResult::finished, jsonReply, [this, jsonReply, result](){ QVariantMap params = statusToReply(result->status()); if (result->status() == Device::DeviceErrorNoError) { @@ -847,12 +838,12 @@ JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap ¶ms) const return jsonReply; } -JsonReply *DeviceHandler::ExecuteAction(const QVariantMap ¶ms) +JsonReply *DeviceHandler::ExecuteAction(const QVariantMap ¶ms, const JsonContext &context) { DeviceId deviceId(params.value("deviceId").toString()); ActionTypeId actionTypeId(params.value("actionTypeId").toString()); ParamList actionParams = unpack(params.value("params")); - QLocale locale = params.value("locale").toLocale(); + QLocale locale = context.locale(); Action action(actionTypeId, deviceId); action.setParams(actionParams); diff --git a/libnymea-core/jsonrpc/devicehandler.h b/libnymea-core/jsonrpc/devicehandler.h index 92d1b3bc..b2aaa2d5 100644 --- a/libnymea-core/jsonrpc/devicehandler.h +++ b/libnymea-core/jsonrpc/devicehandler.h @@ -44,32 +44,32 @@ public: QString name() const override; - Q_INVOKABLE JsonReply *GetSupportedVendors(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetSupportedDevices(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetDiscoveredDevices(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetPlugins(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetSupportedVendors(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetSupportedDevices(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetDiscoveredDevices(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetPlugins(const QVariantMap ¶ms, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetPluginConfiguration(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *SetPluginConfiguration(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *AddConfiguredDevice(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *PairDevice(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *ConfirmPairing(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *AddConfiguredDevice(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *PairDevice(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *ConfirmPairing(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *GetConfiguredDevices(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *ReconfigureDevice(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *ReconfigureDevice(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *EditDevice(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *RemoveConfiguredDevice(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetDeviceSettings(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap ¶ms, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetStateValue(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *GetStateValues(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *BrowseDevice(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *BrowseDevice(const QVariantMap ¶ms, const JsonContext &context) const; + Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap ¶ms, const JsonContext &context) const; - Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap ¶ms); diff --git a/libnymea-core/jsonrpc/eventhandler.cpp b/libnymea-core/jsonrpc/eventhandler.cpp index a3957e5e..524c2a0c 100644 --- a/libnymea-core/jsonrpc/eventhandler.cpp +++ b/libnymea-core/jsonrpc/eventhandler.cpp @@ -93,16 +93,15 @@ void EventHandler::eventTriggered(const Event &event) emit EventTriggered(params); } -JsonReply* EventHandler::GetEventType(const QVariantMap ¶ms) const +JsonReply* EventHandler::GetEventType(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); qCDebug(dcJsonRpc) << "asked for event type" << params; EventTypeId eventTypeId(params.value("eventTypeId").toString()); foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) { foreach (const EventType &eventType, deviceClass.eventTypes()) { if (eventType.id() == eventTypeId) { EventType translatedEventType = eventType; - translatedEventType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), eventType.displayName(), locale)); + translatedEventType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), eventType.displayName(), context.locale())); QVariantMap data; data.insert("deviceError", enumValueName(Device::DeviceErrorNoError)); data.insert("eventType", pack(translatedEventType)); diff --git a/libnymea-core/jsonrpc/eventhandler.h b/libnymea-core/jsonrpc/eventhandler.h index 234e07e5..615ed2b4 100644 --- a/libnymea-core/jsonrpc/eventhandler.h +++ b/libnymea-core/jsonrpc/eventhandler.h @@ -44,7 +44,7 @@ public: explicit EventHandler(QObject *parent = nullptr); QString name() const override; - Q_INVOKABLE JsonReply *GetEventType(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetEventType(const QVariantMap ¶ms, const JsonContext &context) const; signals: void EventTriggered(const QVariantMap ¶ms); diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp index 759eba61..3a260acb 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp @@ -159,7 +159,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("username", enumValueName(String)); params.insert("password", enumValueName(String)); returns.insert("error", enumRef()); - registerMethod("CreateUser", description, params, returns); + registerMethod("CreateUser", description, params, returns, "Use Users.CreateUser instead."); params.clear(); returns.clear(); description = "Authenticate a client to the api via user & password challenge. Provide " @@ -171,7 +171,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("deviceName", enumValueName(String)); returns.insert("success", enumValueName(Bool)); returns.insert("o:token", enumValueName(String)); - registerMethod("Authenticate", description, params, returns); + registerMethod("Authenticate", description, params, returns, "Use Users.Authenticate instead."); params.clear(); returns.clear(); description = "Authenticate a client to the api via Push Button method. " @@ -190,7 +190,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("deviceName", enumValueName(String)); returns.insert("success", enumValueName(Bool)); returns.insert("transactionId", enumValueName(Int)); - registerMethod("RequestPushButtonAuth", description, params, returns); + registerMethod("RequestPushButtonAuth", description, params, returns, "Use Users.RequestPushButtonAuth instead."); params.clear(); returns.clear(); description = "Return a list of TokenInfo objects of all the tokens for the current user."; @@ -246,7 +246,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("success", enumValueName(Bool)); params.insert("transactionId", enumValueName(Int)); params.insert("o:token", enumValueName(String)); - registerNotification("PushButtonAuthFinished", description, params); + registerNotification("PushButtonAuthFinished", description, params, "Use Users.PushButtonAuthFinished instead."); QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); @@ -259,12 +259,12 @@ QString JsonRPCServerImplementation::name() const return QStringLiteral("JSONRPC"); } -JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap ¶ms) +JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap ¶ms, const JsonContext &context) { TransportInterface *interface = reinterpret_cast(property("transportInterface").toLongLong()); qCDebug(dcJsonRpc()) << params; - QUuid clientId = this->property("clientId").toUuid(); + QUuid clientId = context.clientId(); if (params.contains("locale")) { m_clientLocales.insert(clientId, QLocale(params.value("locale").toString())); } @@ -297,9 +297,9 @@ JsonReply* JsonRPCServerImplementation::Version(const QVariantMap ¶ms) const return createReply(data); } -JsonReply* JsonRPCServerImplementation::SetNotificationStatus(const QVariantMap ¶ms) +JsonReply* JsonRPCServerImplementation::SetNotificationStatus(const QVariantMap ¶ms, const JsonContext &context) { - QUuid clientId = this->property("clientId").toUuid(); + QUuid clientId = context.clientId(); Q_ASSERT_X(m_clientTransports.contains(clientId), "JsonRPCServer", "Invalid client ID."); QStringList enabledNamespaces; @@ -351,10 +351,10 @@ JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap ¶ms) return createReply(ret); } -JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap ¶ms) +JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context) { QString deviceName = params.value("deviceName").toString(); - QUuid clientId = this->property("clientId").toUuid(); + QUuid clientId = context.clientId(); int transactionId = NymeaCore::instance()->userManager()->requestPushButtonAuth(deviceName); m_pushButtonTransactions.insert(transactionId, clientId); @@ -366,12 +366,11 @@ JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap return createReply(data); } -JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap ¶ms) const +JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap ¶ms, const JsonContext &context) const { Q_UNUSED(params) - QByteArray token = property("token").toByteArray(); - TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token); + TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token()); QList tokens = NymeaCore::instance()->userManager()->tokens(tokenInfo.username()); QVariantList retList; foreach (const TokenInfo &tokenInfo, tokens) { @@ -636,8 +635,8 @@ 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 = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.CreateUser"}; - QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"}; + QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.RequestPushButtonAuth", "Users.CreateUser"}; + QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth", "Users.Authenticate", "Users.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 (!authExemptMethodsNoUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { @@ -683,13 +682,6 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac return; } - // Hack: attach some properties to the 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()); - handler->setProperty("transportInterface", reinterpret_cast(interface)); - - qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace << method.toLatin1().data(); - if (!(targetNamespace == "JSONRPC" && method == "Hello")) { // This is not the handshake message. If we've waited for it, consider this a protocol violation and drop connection if (m_newConnectionWaitTimers.contains(clientId)) { @@ -698,16 +690,25 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac interface->terminateClientConnection(clientId); return; } - - - // Unless this is the Hello message, which allows setting the locale explicitly, attach the locale - // for this connection - // If the client did request a locale in the Hello message, use that locale - params.insert("locale", m_clientLocales.value(clientId)); } + // Attach the transportInterface if this call is for ourselves + if (handler == this) { + handler->setProperty("transportInterface", reinterpret_cast(interface)); + } + + JsonContext callContext(clientId, m_clientLocales.value(clientId)); + callContext.setToken(message.value("token").toByteArray()); + + qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' + method << "from client" << clientId; + JsonReply *reply; - QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params)); + if (handler->metaObject()->indexOfMethod(method.toUtf8() + "(QVariantMap,JsonContext)") >= 0) { + QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params), Q_ARG(JsonContext, callContext)); + } else { + QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params)); + } + if (reply->type() == JsonReply::TypeAsync) { m_asyncReplies.insert(reply, interface); reply->setClientId(clientId); @@ -755,7 +756,7 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap ¶ms) JsonValidator validator; Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(), validator.result().where().toUtf8(), - validator.result().errorString().toUtf8()); + validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented)); if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) { QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString(); @@ -773,6 +774,39 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap ¶ms) } } +void JsonRPCServerImplementation::sendClientNotification(const QUuid &clientId, const QVariantMap ¶ms) +{ + JsonHandler *handler = qobject_cast(sender()); + QMetaMethod method = handler->metaObject()->method(senderSignalIndex()); + + if (!m_clientTransports.contains(clientId)) { + qCWarning(dcJsonRpc()) << "No client with id" << clientId << ". Not sending client notification."; + return; + } + + QVariantMap notification; + notification.insert("id", m_notificationId++); + notification.insert("notification", handler->name() + "." + method.name()); + notification.insert("params", params); + + JsonValidator validator; + Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(), + validator.result().where().toUtf8(), + validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented)); + + if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) { + QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString(); + qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!"; + qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage; + notification.insert("deprecationWarning", deprecationMessage); + } + + QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact); + qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data; + qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name(); + m_clientTransports.value(clientId)->sendData(clientId, data); +} + void JsonRPCServerImplementation::asyncReplyFinished() { JsonReply *reply = qobject_cast(sender()); @@ -834,12 +868,6 @@ void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bo return; } - TransportInterface *transport = m_clientTransports.value(clientId); - if (!transport) { - qCWarning(dcJsonRpc()) << "No transport for given clientId"; - return; - } - QVariantMap params; params.insert("transactionId", transactionId); params.insert("success", success); @@ -847,12 +875,7 @@ void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bo params.insert("token", token); } - QVariantMap notification; - notification.insert("id", transactionId); - notification.insert("notification", "JSONRPC.PushButtonAuthFinished"); - notification.insert("params", params); - - transport->sendData(clientId, QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact)); + emit PushButtonAuthFinished(clientId, params); } bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler) @@ -914,8 +937,10 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler) QVariantMap newMethods; foreach (const QString &methodName, handler->jsonMethods().keys()) { QVariantMap method = handler->jsonMethods().value(methodName).toMap(); - if (handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap)") < 0) { - qCWarning(dcJsonRpc()).nospace().noquote() << "Invalid method \"" << methodName << "\". Method \"JsonReply* " + methodName + "(QVariantMap)\" does not exist. Not registering handler " << handler->name(); + + if (handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap)") < 0 + && handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap,JsonContext)") < 0) { + qCWarning(dcJsonRpc()).nospace().noquote() << "Invalid method \"" << methodName << "\". Method \"JsonReply* " + methodName + "(QVariantMap,JsonContext)\" does not exist. Not registering handler " << handler->name(); return false; } if (!JsonValidator::checkRefs(method.value("params").toMap(), apiIncludingThis)) { @@ -952,7 +977,11 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler) for (int i = 0; i < handler->metaObject()->methodCount(); ++i) { QMetaMethod method = handler->metaObject()->method(i); if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegExp("^[A-Z]"))) { - QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)"))); + if (method.parameterCount() == 1 && method.parameterType(0) == QVariant::Map) { + QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)"))); + } else if (method.parameterCount() == 2 && method.parameterType(0) == QVariant::Uuid && method.parameterType(1) == QVariant::Map) { + QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QUuid,QVariantMap)"))); + } } } return true; diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h index 38f56521..e319060a 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h @@ -57,15 +57,15 @@ public: // JsonHandler API implementation QString name() const; - Q_INVOKABLE JsonReply *Hello(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *Hello(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *Introspect(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *Version(const QVariantMap ¶ms) const; - Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms, const JsonContext &context) const; Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupCloudConnection(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap ¶ms); @@ -74,7 +74,7 @@ public: signals: void CloudConnectedChanged(const QVariantMap &map); - void PushButtonAuthFinished(const QVariantMap ¶ms); + void PushButtonAuthFinished(const QUuid &clientId, const QVariantMap ¶ms); // Server API public: @@ -103,6 +103,7 @@ private slots: void processData(const QUuid &clientId, const QByteArray &data); void sendNotification(const QVariantMap ¶ms); + void sendClientNotification(const QUuid &clientId, const QVariantMap ¶ms); void asyncReplyFinished(); diff --git a/libnymea-core/jsonrpc/statehandler.cpp b/libnymea-core/jsonrpc/statehandler.cpp index 3ea0e94c..6732447d 100644 --- a/libnymea-core/jsonrpc/statehandler.cpp +++ b/libnymea-core/jsonrpc/statehandler.cpp @@ -70,9 +70,8 @@ QString StateHandler::name() const return "States"; } -JsonReply* StateHandler::GetStateType(const QVariantMap ¶ms) const +JsonReply* StateHandler::GetStateType(const QVariantMap ¶ms, const JsonContext &context) const { - QLocale locale = params.value("locale").toLocale(); qCDebug(dcJsonRpc) << "asked for state type" << params; StateTypeId stateTypeId(params.value("stateTypeId").toString()); foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) { @@ -81,7 +80,7 @@ JsonReply* StateHandler::GetStateType(const QVariantMap ¶ms) const QVariantMap data; data.insert("deviceError", enumValueName(Device::DeviceErrorNoError)); StateType translatedStateType = stateType; - translatedStateType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), stateType.displayName(), locale)); + translatedStateType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), stateType.displayName(), context.locale())); data.insert("stateType", pack(translatedStateType)); return createReply(data); } diff --git a/libnymea-core/jsonrpc/statehandler.h b/libnymea-core/jsonrpc/statehandler.h index 15f4353a..85f9b1b4 100644 --- a/libnymea-core/jsonrpc/statehandler.h +++ b/libnymea-core/jsonrpc/statehandler.h @@ -42,7 +42,7 @@ public: explicit StateHandler(QObject *parent = nullptr); QString name() const override; - Q_INVOKABLE JsonReply *GetStateType(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetStateType(const QVariantMap ¶ms, const JsonContext &context) const; }; diff --git a/libnymea-core/jsonrpc/usershandler.cpp b/libnymea-core/jsonrpc/usershandler.cpp index 6b52e1aa..0bb8d996 100644 --- a/libnymea-core/jsonrpc/usershandler.cpp +++ b/libnymea-core/jsonrpc/usershandler.cpp @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "usershandler.h" #include "usermanager/usermanager.h" #include "usermanager/userinfo.h" @@ -23,6 +53,37 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): returns.insert("error", enumRef()); registerMethod("CreateUser", description, params, returns); + params.clear(); returns.clear(); + description = "Authenticate a client to the api via user & password challenge. Provide " + "a device name which allows the user to identify the client and revoke the token in case " + "the device is lost or stolen. This will return a new token to be used to authorize a " + "client at the API."; + params.insert("username", enumValueName(String)); + params.insert("password", enumValueName(String)); + params.insert("deviceName", enumValueName(String)); + returns.insert("success", enumValueName(Bool)); + returns.insert("o:token", enumValueName(String)); + registerMethod("Authenticate", description, params, returns); + + params.clear(); returns.clear(); + description = "Authenticate a client to the api via Push Button method. " + "Provide a device name which allows the user to identify the client and revoke the " + "token in case the device is lost or stolen. If push button hardware is available, " + "this will return with success and start listening for push button presses. When the " + "push button is pressed, the PushButtonAuthFinished notification will be sent to the " + "requesting client. The procedure will be cancelled when the connection is interrupted. " + "If another client requests push button authentication while a procedure is still going " + "on, the second call will take over and the first one will be notified by the " + "PushButtonAuthFinished signal about the error. The application should make it clear " + "to the user to not press the button when the procedure fails as this can happen for 2 " + "reasons: a) a second user is trying to auth at the same time and only the currently " + "active user should press the button or b) it might indicate an attacker trying to take " + "over and snooping in for tokens."; + params.insert("deviceName", enumValueName(String)); + returns.insert("success", enumValueName(Bool)); + returns.insert("transactionId", enumValueName(Int)); + registerMethod("RequestPushButtonAuth", description, params, returns); + params.clear(); returns.clear(); description = "Change the password for the currently logged in user."; params.insert("newPassword", enumValueName(String)); @@ -47,6 +108,18 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): returns.insert("error", enumRef()); registerMethod("RemoveToken", description, params, returns); + // Notifications + params.clear(); + description = "Emitted when a push button authentication reaches final state. NOTE: This notification is " + "special. It will only be emitted to connections that did actively request a push button " + "authentication, but also it will be emitted regardless of the notification settings."; + params.insert("success", enumValueName(Bool)); + params.insert("transactionId", enumValueName(Int)); + params.insert("o:token", enumValueName(String)); + registerNotification("PushButtonAuthFinished", description, params); + + connect(m_userManager, &UserManager::pushButtonAuthFinished, this, &UsersHandler::onPushButtonAuthFinished); + } QString UsersHandler::name() const @@ -66,11 +139,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap ¶ms) return createReply(returns); } -JsonReply *UsersHandler::ChangePassword(const QVariantMap ¶ms) +JsonReply *UsersHandler::ChangePassword(const QVariantMap ¶ms, const JsonContext &context) { QVariantMap ret; - QByteArray currentToken = property("token").toByteArray(); + QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection"; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); @@ -106,13 +179,12 @@ JsonReply *UsersHandler::Authenticate(const QVariantMap ¶ms) return createReply(ret); } -JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap ¶ms) +JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context) { QString deviceName = params.value("deviceName").toString(); - QUuid clientId = this->property("clientId").toUuid(); int transactionId = m_userManager->requestPushButtonAuth(deviceName); - m_pushButtonTransactions.insert(transactionId, clientId); + m_pushButtonTransactions.insert(transactionId, context.clientId()); QVariantMap data; data.insert("transactionId", transactionId); @@ -121,12 +193,12 @@ JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap ¶ms) return createReply(data); } -JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms) +JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms, const JsonContext &context) { Q_UNUSED(params) QVariantMap ret; - QByteArray currentToken = property("token").toByteArray(); + QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { qCWarning(dcJsonRpc()) << "Cannot get user info form an unauthenticated connection"; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); @@ -145,12 +217,12 @@ JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms) return createReply(ret); } -JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms) +JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms, const JsonContext &context) { Q_UNUSED(params) QVariantMap ret; - QByteArray currentToken = property("token").toByteArray(); + QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { qCWarning(dcJsonRpc()) << "Cannot fetch tokens form an unauthenticated connection"; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); @@ -175,11 +247,11 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms) return createReply(ret); } -JsonReply *UsersHandler::RemoveToken(const QVariantMap ¶ms) +JsonReply *UsersHandler::RemoveToken(const QVariantMap ¶ms, const JsonContext &context) { QVariantMap ret; - QByteArray currentToken = property("token").toByteArray(); + QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection."; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); @@ -213,4 +285,24 @@ JsonReply *UsersHandler::RemoveToken(const QVariantMap ¶ms) return createReply(ret); } +void UsersHandler::onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token) +{ + Q_UNUSED(success) + Q_UNUSED(token) + QUuid clientId = m_pushButtonTransactions.take(transactionId); + if (clientId.isNull()) { + qCDebug(dcJsonRpc()) << "Received a PushButton reply but wasn't expecting it."; + return; + } + + QVariantMap params; + params.insert("transactionId", transactionId); + params.insert("success", success); + if (success) { + params.insert("token", token); + } + + emit PushButtonAuthFinished(clientId, params); +} + } diff --git a/libnymea-core/jsonrpc/usershandler.h b/libnymea-core/jsonrpc/usershandler.h index ddcd28bb..10159720 100644 --- a/libnymea-core/jsonrpc/usershandler.h +++ b/libnymea-core/jsonrpc/usershandler.h @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* 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 USERSHANDLER_H #define USERSHANDLER_H @@ -18,15 +48,18 @@ public: QString name() const override; Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *GetTokens(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *GetTokens(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms, const JsonContext &context); signals: - void PushButtonAuthFinished(const QVariantMap ¶ms); + void PushButtonAuthFinished(const QUuid &clientId, const QVariantMap ¶ms); + +private slots: + void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); private: UserManager *m_userManager = nullptr; diff --git a/libnymea/jsonrpc/jsoncontext.cpp b/libnymea/jsonrpc/jsoncontext.cpp new file mode 100644 index 00000000..fc73cac3 --- /dev/null +++ b/libnymea/jsonrpc/jsoncontext.cpp @@ -0,0 +1,58 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "jsoncontext.h" + +JsonContext::JsonContext(const QUuid &clientId, const QLocale &locale): + m_clientId(clientId), + m_locale(locale) +{ + +} + +QUuid JsonContext::clientId() const +{ + return m_clientId; +} + +QLocale JsonContext::locale() const +{ + return m_locale; +} + +QByteArray JsonContext::token() const +{ + return m_token; +} + +void JsonContext::setToken(const QByteArray &token) +{ + m_token = token; +} diff --git a/libnymea/jsonrpc/jsoncontext.h b/libnymea/jsonrpc/jsoncontext.h new file mode 100644 index 00000000..95d097e3 --- /dev/null +++ b/libnymea/jsonrpc/jsoncontext.h @@ -0,0 +1,54 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* 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 JSONCONTEXT_H +#define JSONCONTEXT_H + +#include +#include + +class JsonContext +{ +public: + JsonContext(const QUuid &clientId, const QLocale &locale); + + QUuid clientId() const; + QLocale locale() const; + + QByteArray token() const; + void setToken(const QByteArray &token); + +private: + QUuid m_clientId; + QLocale m_locale; + QByteArray m_token; +}; + +#endif // JSONCONTEXT_H diff --git a/libnymea/jsonrpc/jsonhandler.h b/libnymea/jsonrpc/jsonhandler.h index f7bbe7a6..ec419221 100644 --- a/libnymea/jsonrpc/jsonhandler.h +++ b/libnymea/jsonrpc/jsonhandler.h @@ -39,6 +39,7 @@ #include #include "jsonreply.h" +#include "jsoncontext.h" class JsonHandler : public QObject { @@ -108,7 +109,6 @@ protected: JsonReply *createAsyncReply(const QString &method) const; private: - void registerObject(const QMetaObject &metaObject); void registerObject(const QMetaObject &metaObject, const QMetaObject &listMetaObject); diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index 0e6ca52d..3d5763ba 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -24,6 +24,7 @@ HEADERS += \ devices/devicepairinginfo.h \ devices/deviceactioninfo.h \ devices/browseresult.h \ + jsonrpc/jsoncontext.h \ jsonrpc/jsonhandler.h \ jsonrpc/jsonreply.h \ jsonrpc/jsonrpcserver.h \ @@ -106,6 +107,7 @@ SOURCES += \ devices/devicepairinginfo.cpp \ devices/deviceactioninfo.cpp \ devices/browseresult.cpp \ + jsonrpc/jsoncontext.cpp \ jsonrpc/jsonhandler.cpp \ jsonrpc/jsonreply.cpp \ jsonrpc/jsonrpcserver.cpp \ diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index 1c706d94..885b9de0 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -34,6 +34,7 @@ #include "version.h" #include "servers/mocktcpserver.h" #include "usermanager/usermanager.h" +#include "nymeadbusservice.h" using namespace nymeaserver; @@ -151,6 +152,7 @@ QStringList TestJSONRPC::extractRefs(const QVariant &variant) void TestJSONRPC::initTestCase() { + NymeaDBusService::setBusType(QDBusConnection::SessionBus); NymeaTestBase::initTestCase(); QLoggingCategory::setFilterRules("*.debug=false\n" // "JsonRpcTraffic.debug=true\n" @@ -182,7 +184,7 @@ void TestJSONRPC::testHandshake() // Now register push button agent PushButtonAgent pushButtonAgent; - pushButtonAgent.init(); + pushButtonAgent.init(QDBusConnection::SessionBus); // And now check if it is sent again when calling JSONRPC.Hello handShake = injectAndWait("JSONRPC.Hello").toMap(); @@ -1141,7 +1143,7 @@ void TestJSONRPC::pluginConfigChangeEmitsNotification() void TestJSONRPC::testPushButtonAuth() { PushButtonAgent pushButtonAgent; - pushButtonAgent.init(); + pushButtonAgent.init(QDBusConnection::SessionBus); QVariantMap params; params.insert("deviceName", "pbtestdevice"); @@ -1167,7 +1169,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt() { enableNotifications({}); PushButtonAgent pushButtonAgent; - pushButtonAgent.init(); + pushButtonAgent.init(QDBusConnection::SessionBus); // m_clientId is registered in gutTestbase already, just using it here to improve readability of the test QUuid aliceId = m_clientId; @@ -1272,7 +1274,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt() void TestJSONRPC::testPushButtonAuthConnectionDrop() { PushButtonAgent pushButtonAgent; - pushButtonAgent.init(); + pushButtonAgent.init(QDBusConnection::SessionBus); // Snoop in on everything the TCP server sends to its clients. QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData); @@ -1339,7 +1341,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth() QVERIFY(spy.isValid()); PushButtonAgent pushButtonAgent; - pushButtonAgent.init(); + pushButtonAgent.init(QDBusConnection::SessionBus); // Hello call should work in any case, telling us initial setup is required spy.clear(); diff --git a/tests/auto/usermanager/testusermanager.cpp b/tests/auto/usermanager/testusermanager.cpp index 6a7e1a6f..7b12bdcb 100644 --- a/tests/auto/usermanager/testusermanager.cpp +++ b/tests/auto/usermanager/testusermanager.cpp @@ -35,6 +35,9 @@ #include "nymeatestbase.h" #include "usermanager/usermanager.h" #include "servers/mocktcpserver.h" +#include "nymeadbusservice.h" + +#include "../../utils/pushbuttonagent.h" using namespace nymeaserver; @@ -45,14 +48,50 @@ public: TestUsermanager(QObject* parent = nullptr); private slots: + void initTestCase(); + void init(); void loginValidation_data(); void loginValidation(); void createUser(); + void authenticate(); + /* + Cases for push button auth: + + Case 1: regular pushbutton + - alice sends Users.RequestPushButtonAuth, gets "OK" back (if push button hardware is available) + - alice pushes the hardware button and gets a notification on jsonrpc containing the token for local auth + */ + void authenticatePushButton(); + + /* + Case 2: if we have an attacker in the network, he could try to call requestPushButtonAuth and + hope someone would eventually press the button and give him a token. In order to prevent this, + any previous attempt for a push button auth needs to be cancelled when a new request comes in: + + * Mallory does RequestPushButtonAuth, gets OK back + * Alice does RequestPushButtonAuth, + * Mallory receives a "PushButtonFailed" notification + * Alice receives OK + * Alice presses the hardware button + * Alice reveices a notification with token, mallory receives nothing + + Case 3: Mallory tries to hijack it back again + + * Mallory does RequestPushButtonAuth, gets OK back + * Alice does RequestPusButtonAuth, + * Alice gets ok reply, Mallory gets failed notification + * Mallory quickly does RequestPushButtonAuth again to win the fight + * Alice gets failed notification and can instruct the user to _not_ press the button now until procedure is restarted + */ + void authenticatePushButtonAuthInterrupt(); + + void authenticatePushButtonAuthConnectionDrop(); + void createDuplicateUser(); void getTokens(); @@ -79,6 +118,12 @@ TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent) QCoreApplication::instance()->setOrganizationName("nymea-test"); } +void TestUsermanager::initTestCase() +{ + NymeaDBusService::setBusType(QDBusConnection::SessionBus); + NymeaTestBase::initTestCase(); +} + void TestUsermanager::init() { UserManager *userManager = NymeaCore::instance()->userManager(); @@ -155,7 +200,7 @@ void TestUsermanager::authenticate() params.insert("username", "valid@user.test"); params.insert("password", "Bla1234*"); params.insert("deviceName", "autotests"); - QVariant response = injectAndWait("JSONRPC.Authenticate", params); + QVariant response = injectAndWait("Users.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); @@ -163,6 +208,192 @@ void TestUsermanager::authenticate() QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating"); } +void TestUsermanager::authenticatePushButton() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(QDBusConnection::SessionBus); + + QVariantMap params; + params.insert("deviceName", "pbtestdevice"); + QVariant response = injectAndWait("Users.RequestPushButtonAuth", params); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Setup connection to mock client + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + pushButtonAgent.sendButtonPressed(); + + if (clientSpy.count() == 0) clientSpy.wait(); + QVariantMap rsp = checkNotification(clientSpy, "Users.PushButtonAuthFinished").toMap(); + + QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId); + QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification"); +} + +void TestUsermanager::authenticatePushButtonAuthInterrupt() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(QDBusConnection::SessionBus); + + // m_clientId is registered in gutTestbase already, just using it here to improve readability of the test + QUuid aliceId = m_clientId; + + // Create a new clientId for mallory and connect it to the server + QUuid malloryId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(malloryId); + QSignalSpy responseSpy(m_mockTcpServer, &MockTcpServer::outgoingData); + m_mockTcpServer->injectData(malloryId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); + if (responseSpy.count() == 0) responseSpy.wait(); + + // Snoop in on everything the TCP server sends to its clients. + QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); + + // request push button auth for client 1 (alice) and check for OK reply + QVariantMap params; + params.insert("deviceName", "alice"); + QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId1 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + + // Request push button auth for client 2 (mallory) + clientSpy.clear(); + params.clear(); + params.insert("deviceName", "mallory"); + response = injectAndWait("Users.RequestPushButtonAuth", params, malloryId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Both clients should receive something. Wait for it + if (clientSpy.count() < 2) { + clientSpy.wait(); + } + + // spy.at(0) should be the failed notification for alice + // spy.at(1) shpuld be the OK reply for mallory + + + // alice should have received a failed notification. She knows something's wrong. + QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), aliceId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId1); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); + + // Mallory instead should have received an OK + QVariantMap reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.at(1).first().toUuid(), malloryId); + QCOMPARE(reply.value("params").toMap().value("success").toBool(), true); + + + // Alice tries once more + clientSpy.clear(); + params.clear(); + params.insert("deviceName", "alice"); + response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId3 = response.toMap().value("params").toMap().value("transactionId").toInt(); + + // Both clients should receive something. Wait for it + if (clientSpy.count() < 2) { + clientSpy.wait(); + } + + // spy.at(0) should be the failed notification for mallory + // spy.at(1) shpuld be the OK reply for alice + + // mallory should have received a failed notification. She knows something's wrong. + notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), malloryId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId2); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); + + // Alice instead should have received an OK + reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.at(1).first().toUuid(), aliceId); + QCOMPARE(reply.value("params").toMap().value("success").toBool(), true); + + clientSpy.clear(); + + // do the button press + pushButtonAgent.sendButtonPressed(); + + // Wait for things to happen + if (clientSpy.count() == 0) { + clientSpy.wait(); + } + + // There should have been only exactly one message sent, the token for alice + // Mallory should not have received anything + QCOMPARE(clientSpy.count(), 1); + notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), aliceId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); + QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); +} + +void TestUsermanager::authenticatePushButtonAuthConnectionDrop() +{ + PushButtonAgent pushButtonAgent; + pushButtonAgent.init(QDBusConnection::SessionBus); + + // Snoop in on everything the TCP server sends to its clients. + QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData); + + // Create a new clientId for alice and connect it to the server + QUuid aliceId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(aliceId); + m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); + if (clientSpy.count() == 0) clientSpy.wait(); + + // request push button auth for client 1 (alice) and check for OK reply + QVariantMap params; + params.insert("deviceName", "alice"); + QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + + // Disconnect alice + m_mockTcpServer->clientDisconnected(aliceId); + + // Now try with bob + // Create a new clientId for bob and connect it to the server + QUuid bobId = QUuid::createUuid(); + m_mockTcpServer->clientConnected(bobId); + clientSpy.clear(); + m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); + if (clientSpy.count() == 0) clientSpy.wait(); + + // request push button auth for client 2 (bob) and check for OK reply + params.clear(); + params.insert("deviceName", "bob"); + response = injectAndWait("Users.RequestPushButtonAuth", params, bobId); + QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); + int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); + + clientSpy.clear(); + + pushButtonAgent.sendButtonPressed(); + + // Wait for things to happen + if (clientSpy.count() == 0) { + clientSpy.wait(); + } + + // There should have been only exactly one message sent, the token for bob + QCOMPARE(clientSpy.count(), 1); + QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); + QCOMPARE(clientSpy.first().first().toUuid(), bobId); + QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId); + QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); + QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); + +} + void TestUsermanager::createDuplicateUser() { authenticate(); @@ -223,7 +454,7 @@ void TestUsermanager::authenticateAfterPasswordChangeOK() 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); + QVariant response = injectAndWait("Users.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); QVERIFY2(!m_apiToken.isEmpty(), "Token should not be empty"); @@ -239,7 +470,7 @@ void TestUsermanager::authenticateAfterPasswordChangeFail() 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); + QVariant response = injectAndWait("Users.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); QVERIFY2(m_apiToken.isEmpty(), "Token should be empty"); diff --git a/tests/auto/usermanager/usermanager.pro b/tests/auto/usermanager/usermanager.pro index e42391e3..9744201d 100644 --- a/tests/auto/usermanager/usermanager.pro +++ b/tests/auto/usermanager/usermanager.pro @@ -2,4 +2,9 @@ include(../../../nymea.pri) include(../autotests.pri) TARGET = testusermanager -SOURCES += testusermanager.cpp + +HEADERS += ../../utils/pushbuttonagent.h + +SOURCES += testusermanager.cpp \ + ../../utils/pushbuttonagent.cpp +