Add thing added and removed logic depending on users thing permission

This commit is contained in:
Simon Stürz 2025-11-03 15:17:26 +01:00
parent b80ad6d839
commit 360e287619
9 changed files with 253 additions and 127 deletions

View File

@ -446,7 +446,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){ connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){
QVariantMap params; QVariantMap params;
params.insert("event", pack(event)); params.insert("event", pack(event));
emit EventTriggered(params); emit EventTriggered(params, event.thingId());
}); });
params.clear(); returns.clear(); params.clear(); returns.clear();
@ -519,6 +519,21 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex(); hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex();
m_cacheHashes.insert("GetPlugins", hash); m_cacheHashes.insert("GetPlugins", hash);
}); });
connect(NymeaCore::instance()->userManager(), &UserManager::userThingRestrictionsChanged, this, [this](const UserInfo &userInfo, const ThingId &thingId, bool accessGranted){
if (accessGranted) {
QVariantMap params;
params.insert("thing", pack(m_thingManager->findConfiguredThing(thingId)));
emit ThingAdded(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been granted.";
} else {
QVariantMap params;
params.insert("thingId", thingId);
emit ThingRemoved(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been dropped.";
}
});
} }
QString IntegrationsHandler::name() const QString IntegrationsHandler::name() const
@ -1143,8 +1158,7 @@ JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params, cons
IOConnections ioConnections = m_thingManager->ioConnections(thingId); IOConnections ioConnections = m_thingManager->ioConnections(thingId);
QVariantMap returns; QVariantMap returns;
QVariant bla = pack(ioConnections); returns.insert("ioConnections", pack(ioConnections));
returns.insert("ioConnections", pack(ioConnections));
returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorNoError)); returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorNoError));
return createReply(returns); return createReply(returns);
} }
@ -1214,28 +1228,28 @@ void IntegrationsHandler::thingStateChanged(Thing *thing, const QUuid &stateType
params.insert("minValue", minValue); params.insert("minValue", minValue);
params.insert("maxValue", maxValue); params.insert("maxValue", maxValue);
params.insert("possibleValues", possibleValues); params.insert("possibleValues", possibleValues);
emit StateChanged(params); emit StateChanged(params, thing->id());
} }
void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId) void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId)
{ {
QVariantMap params; QVariantMap params;
params.insert("thingId", thingId); params.insert("thingId", thingId);
emit ThingRemoved(params); emit ThingRemoved(params, thingId);
} }
void IntegrationsHandler::thingAddedNotification(Thing *thing) void IntegrationsHandler::thingAddedNotification(Thing *thing)
{ {
QVariantMap params; QVariantMap params;
params.insert("thing", pack(thing)); params.insert("thing", pack(thing));
emit ThingAdded(params); emit ThingAdded(params, thing->id());
} }
void IntegrationsHandler::thingChangedNotification(Thing *thing) void IntegrationsHandler::thingChangedNotification(Thing *thing)
{ {
QVariantMap params; QVariantMap params;
params.insert("thing", pack(thing)); params.insert("thing", pack(thing));
emit ThingChanged(params); emit ThingChanged(params, thing->id());
} }
void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId &paramTypeId, const QVariant &value) void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId &paramTypeId, const QVariant &value)
@ -1244,7 +1258,7 @@ void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId
params.insert("thingId", thingId); params.insert("thingId", thingId);
params.insert("paramTypeId", paramTypeId.toString()); params.insert("paramTypeId", paramTypeId.toString());
params.insert("value", value); params.insert("value", value);
emit ThingSettingChanged(params); emit ThingSettingChanged(params, thingId);
} }
QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const

View File

@ -26,6 +26,7 @@
#define INTEGRATIONSHANDLER_H #define INTEGRATIONSHANDLER_H
#include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonhandler.h"
#include "usermanager/userinfo.h"
#include "integrations/thingmanager.h" #include "integrations/thingmanager.h"
namespace nymeaserver { namespace nymeaserver {
@ -80,15 +81,20 @@ public:
signals: signals:
void PluginConfigurationChanged(const QVariantMap &params); void PluginConfigurationChanged(const QVariantMap &params);
void StateChanged(const QVariantMap &params); // Thing permission relevant notifications
void ThingRemoved(const QVariantMap &params); void StateChanged(const QVariantMap &params, const ThingId &thingId);
void ThingAdded(const QVariantMap &params); void ThingRemoved(const QVariantMap &params, const ThingId &thingId);
void ThingChanged(const QVariantMap &params); void ThingAdded(const QVariantMap &params, const ThingId &thingId);
void ThingSettingChanged(const QVariantMap &params); void ThingChanged(const QVariantMap &params, const ThingId &thingId);
void EventTriggered(const QVariantMap &params); void ThingSettingChanged(const QVariantMap &params, const ThingId &thingId);
void EventTriggered(const QVariantMap &params, const ThingId &thingId);
void IOConnectionAdded(const QVariantMap &params); void IOConnectionAdded(const QVariantMap &params);
void IOConnectionRemoved(const QVariantMap &params); void IOConnectionRemoved(const QVariantMap &params);
// User specific notifications depending on the thing based permissions
void ThingRemoved(const QVariantMap &params, const nymeaserver::UserInfo &userInfo);
void ThingAdded(const QVariantMap &params, const nymeaserver::UserInfo &userInfo);
private slots: private slots:
void pluginConfigChanged(const PluginId &id, const ParamList &config); void pluginConfigChanged(const PluginId &id, const ParamList &config);

View File

@ -44,19 +44,11 @@
#include "jsonvalidator.h" #include "jsonvalidator.h"
#include "nymeacore.h" #include "nymeacore.h"
#include "usermanager/usermanager.h" #include "usermanager/usermanager.h"
#include "integrations/thingmanager.h"
#include "integrations/integrationplugin.h"
#include "integrations/thing.h"
#include "types/thingclass.h"
#include "ruleengine/rule.h"
#include "ruleengine/ruleengine.h"
#include "loggingcategories.h" #include "loggingcategories.h"
#include "platform/platform.h"
#include "version.h" #include "version.h"
#include "integrationshandler.h" #include "integrationshandler.h"
#include "ruleshandler.h" #include "ruleshandler.h"
#include "scriptshandler.h"
#include "logginghandler.h" #include "logginghandler.h"
#include "configurationhandler.h" #include "configurationhandler.h"
#include "networkmanagerhandler.h" #include "networkmanagerhandler.h"
@ -104,19 +96,19 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
// Methods // Methods
QString description; QVariantMap returns; QVariantMap params; QString description; QVariantMap returns; QVariantMap params;
description = "Initiates a connection. Use this method to perform an initial handshake of the " description = "Initiates a connection. Use this method to perform an initial handshake of the "
"connection. Optionally, a parameter \"locale\" is can be passed to set up the used " "connection. Optionally, a parameter \"locale\" is can be passed to set up the used "
"locale for this connection. Strings such as ThingClass displayNames etc will be " "locale for this connection. Strings such as ThingClass displayNames etc will be "
"localized to this locale. If this parameter is omitted, the default system locale " "localized to this locale. If this parameter is omitted, the default system locale "
"(depending on the configuration) is used. The reply of this method contains information " "(depending on the configuration) is used. The reply of this method contains information "
"about this core instance such as version information, uuid and its name. The locale value" "about this core instance such as version information, uuid and its name. The locale value"
"indicates the locale used for this connection. Note: This method can be called multiple " "indicates the locale used for this connection. Note: This method can be called multiple "
"times. The locale used in the last call for this connection will be used. Other values, " "times. The locale used in the last call for this connection will be used. Other values, "
"like initialSetupRequired might change if the setup has been performed in the meantime.\n " "like initialSetupRequired might change if the setup has been performed in the meantime.\n "
"The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for " "The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for "
"a method does not change, a client may use a previously cached copy of the call instead of " "a method does not change, a client may use a previously cached copy of the call instead of "
"fetching the content again. While the Hello call doesn't necessarily require a token, this " "fetching the content again. While the Hello call doesn't necessarily require a token, this "
"can be called with a token. If a token is provided, it will be verified and the reply contains " "can be called with a token. If a token is provided, it will be verified and the reply contains "
"information about the tokens validity and the user and permissions for the given token."; "information about the tokens validity and the user and permissions for the given token.";
params.insert("o:locale", enumValueName(String)); params.insert("o:locale", enumValueName(String));
returns.insert("server", enumValueName(String)); returns.insert("server", enumValueName(String));
returns.insert("name", enumValueName(String)); returns.insert("name", enumValueName(String));
@ -152,13 +144,13 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Enable/Disable notifications for this connections. Either \"enabled\" or """ description = "Enable/Disable notifications for this connections. Either \"enabled\" or """
"\"namespaces\" needs to be given but not both of them. The boolean based " "\"namespaces\" needs to be given but not both of them. The boolean based "
"\"enabled\" parameter will enable/disable all notifications at once. If " "\"enabled\" parameter will enable/disable all notifications at once. If "
"instead the list-based \"namespaces\" parameter is provided, all given namespaces" "instead the list-based \"namespaces\" parameter is provided, all given namespaces"
"will be enabled, the others will be disabled. The return value of \"success\" will " "will be enabled, the others will be disabled. The return value of \"success\" will "
"indicate success of the operation. The \"enabled\" property in the return value is " "indicate success of the operation. The \"enabled\" property in the return value is "
"deprecated and used for legacy compatibilty only. It will be set to true if at least " "deprecated and used for legacy compatibilty only. It will be set to true if at least "
"one namespace has been enabled."; "one namespace has been enabled.";
params.insert("o:namespaces", enumValueName(StringList)); params.insert("o:namespaces", enumValueName(StringList));
params.insert("d:o:enabled", enumValueName(Bool)); params.insert("d:o:enabled", enumValueName(Bool));
returns.insert("namespaces", enumValueName(StringList)); returns.insert("namespaces", enumValueName(StringList));
@ -178,9 +170,9 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Authenticate a client to the api via user & password challenge. Provide " 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 " "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 " "the device is lost or stolen. This will return a new token to be used to authorize a "
"client at the API."; "client at the API.";
params.insert("username", enumValueName(String)); params.insert("username", enumValueName(String));
params.insert("password", enumValueName(String)); params.insert("password", enumValueName(String));
params.insert("deviceName", enumValueName(String)); params.insert("deviceName", enumValueName(String));
@ -192,18 +184,18 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Authenticate a client to the api via Push Button method. " 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 " "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, " "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 " "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 " "push button is pressed, the PushButtonAuthFinished notification will be sent to the "
"requesting client. The procedure will be cancelled when the connection is interrupted. " "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 " "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 " "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 " "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 " "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 " "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 " "active user should press the button or b) it might indicate an attacker trying to take "
"over and snooping in for tokens."; "over and snooping in for tokens.";
params.insert("deviceName", enumValueName(String)); params.insert("deviceName", enumValueName(String));
returns.insert("success", enumValueName(Bool)); returns.insert("success", enumValueName(Bool));
returns.insert("transactionId", enumValueName(Int)); returns.insert("transactionId", enumValueName(Int));
@ -228,6 +220,8 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
connect(NymeaCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServerImplementation::onPushButtonAuthFinished); connect(NymeaCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServerImplementation::onPushButtonAuthFinished);
m_connectionLockdownTimer.setSingleShot(true); m_connectionLockdownTimer.setSingleShot(true);
m_connectionLockdownTimer.setInterval(3000); m_connectionLockdownTimer.setInterval(3000);
} }
@ -801,6 +795,69 @@ void JsonRPCServerImplementation::sendClientNotification(const QUuid &clientId,
m_clientTransports.value(clientId)->sendData(clientId, data); m_clientTransports.value(clientId)->sendData(clientId, data);
} }
void JsonRPCServerImplementation::sendClientNotification(const QVariantMap &params, const ThingId &thingId)
{
JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
QMetaMethod method = handler->metaObject()->method(senderSignalIndex());
QVariantMap notification;
notification.insert("id", m_notificationId++);
notification.insert("notification", handler->name() + "." + method.name());
foreach (const QUuid &clientId, m_clientNotifications.keys()) {
// Check if this client wants to be notified
if (!m_clientNotifications.value(clientId).contains(handler->name()))
continue;
// Make sure this client is allowed to receive this notification
if (m_clientTokens.contains(clientId)) {
const QByteArray token = m_clientTokens.value(clientId);
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, token)) {
qCDebug(dcJsonRpc()) << "Not sending notification to client" << "to client" << clientId.toString()
<< "due to missing thing permissions" << handler->name() + "." + method.name();
continue;
}
}
// Add deprecation warning if necessary
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" << clientId << "uses deprecated API. Please update client implementation!";
qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
notification.insert("deprecationWarning", deprecationMessage);
}
QLocale locale = m_clientLocales.value(clientId);
QVariantMap translatedParams = handler->translateNotification(method.name(), params, locale);
JsonValidator validator;
Q_ASSERT_X(validator.validateNotificationParams(translatedParams, handler->name() + '.' + method.name(), m_api).success(),
validator.result().where().toUtf8(),
validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(translatedParams).toJson(QJsonDocument::Indented));
notification.insert("params", translatedParams);
QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpc()) << "Sending notification" << handler->name() + "." + method.name() << "to client" << clientId;
qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;
m_clientTransports.value(clientId)->sendData(clientId, data);
}
}
void JsonRPCServerImplementation::sendClientNotification(const QVariantMap &params, const UserInfo &userInfo)
{
// Send client specific notifications
qCDebug(dcJsonRpc()) << "Sending notification to client" << userInfo.username() << "connections...";
foreach (const QByteArray &token, m_clientTokens) {
if (NymeaCore::instance()->userManager()->tokenInfo(token).username() == userInfo.username()) {
sendClientNotification(m_clientTokens.key(token), params);
}
}
}
void JsonRPCServerImplementation::asyncReplyFinished() void JsonRPCServerImplementation::asyncReplyFinished()
{ {
JsonReply *reply = qobject_cast<JsonReply *>(sender()); JsonReply *reply = qobject_cast<JsonReply *>(sender());
@ -980,9 +1037,17 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler)
QMetaMethod method = handler->metaObject()->method(i); QMetaMethod method = handler->metaObject()->method(i);
if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegularExpression("^[A-Z]"))) { if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegularExpression("^[A-Z]"))) {
if (method.parameterCount() == 1 && method.parameterType(0) == QMetaType::QVariantMap) { if (method.parameterCount() == 1 && method.parameterType(0) == QMetaType::QVariantMap) {
// Generic notification for all subscribed clients
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)"))); QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)")));
} else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QUuid && method.parameterType(1) == QMetaType::QVariantMap) { } else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QUuid && method.parameterType(1) == QMetaType::QVariantMap) {
// Notifications for a specific client with the given UUID
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QUuid,QVariantMap)"))); QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QUuid,QVariantMap)")));
} else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QVariantMap && method.parameterType(1) == QMetaType::type("ThingId")) {
// Notifications which contains thing specific information which might be restricted for certain clients
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QVariantMap,ThingId)")));
} else if (method.parameterCount() == 2 && method.parameterType(0) == QMetaType::QVariantMap && method.parameterType(1) == QMetaType::type("nymeaserver::UserInfo")) {
// Notifications for a specific user
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QVariantMap,nymeaserver::UserInfo)")));
} }
} }
} }
@ -1012,6 +1077,7 @@ void JsonRPCServerImplementation::clientConnected(const QUuid &clientId)
m_newConnectionWaitTimers.remove(clientId); m_newConnectionWaitTimers.remove(clientId);
interface->terminateClientConnection(clientId); interface->terminateClientConnection(clientId);
}); });
m_newConnectionWaitTimers.insert(clientId, timer); m_newConnectionWaitTimers.insert(clientId, timer);
timer->start(10000); timer->start(10000);
} }
@ -1024,9 +1090,11 @@ void JsonRPCServerImplementation::clientDisconnected(const QUuid &clientId)
m_clientBuffers.remove(clientId); m_clientBuffers.remove(clientId);
m_clientLocales.remove(clientId); m_clientLocales.remove(clientId);
m_clientTokens.remove(clientId); m_clientTokens.remove(clientId);
if (m_pushButtonTransactions.values().contains(clientId)) { if (m_pushButtonTransactions.values().contains(clientId)) {
NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId)); NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId));
} }
if (m_newConnectionWaitTimers.contains(clientId)) { if (m_newConnectionWaitTimers.contains(clientId)) {
delete m_newConnectionWaitTimers.take(clientId); delete m_newConnectionWaitTimers.take(clientId);
} }

View File

@ -27,12 +27,9 @@
#include "jsonrpc/jsonrpcserver.h" #include "jsonrpc/jsonrpcserver.h"
#include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonhandler.h"
#include "usermanager/userinfo.h"
#include "transportinterface.h" #include "transportinterface.h"
#include "types/thingclass.h"
#include "types/action.h"
#include "types/event.h"
#include <QObject> #include <QObject>
#include <QVariantMap> #include <QVariantMap>
#include <QString> #include <QString>
@ -90,6 +87,8 @@ private slots:
void sendNotification(const QVariantMap &params); void sendNotification(const QVariantMap &params);
void sendClientNotification(const QUuid &clientId, const QVariantMap &params); void sendClientNotification(const QUuid &clientId, const QVariantMap &params);
void sendClientNotification(const QVariantMap &params, const ThingId &thingId);
void sendClientNotification(const QVariantMap &params, const nymeaserver::UserInfo &userInfo);
void asyncReplyFinished(); void asyncReplyFinished();

View File

@ -160,10 +160,11 @@ void NymeaCore::init(const QStringList &additionalInterfaces, bool disableLogEng
m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), this); m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), this);
connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName); connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName);
connect(m_thingManager, &ThingManagerImplementation::loaded, this, &NymeaCore::thingManagerLoaded); connect(m_thingManager, &ThingManagerImplementation::loaded, this, &NymeaCore::thingManagerLoaded);
connect(m_thingManager, &ThingManagerImplementation::thingRemoved, m_userManager, &UserManager::onThingRemoved);
m_logger->log({"started"}, {{"version", NYMEA_VERSION_STRING}}); m_logger->log({"started"}, {{"version", NYMEA_VERSION_STRING}});
#ifdef WITH_SYSTEMD #ifdef WITH_SYSTEMD
sd_notify(0, "READY=1"); sd_notify(0, "READY=1");
#endif #endif
@ -296,7 +297,7 @@ QStringList NymeaCore::loggingFiltersPlugins()
QStringList loggingFiltersPlugins; QStringList loggingFiltersPlugins;
foreach (const QJsonObject &pluginMetadata, ThingManagerImplementation::pluginsMetadata()) { foreach (const QJsonObject &pluginMetadata, ThingManagerImplementation::pluginsMetadata()) {
QString pluginName = pluginMetadata.value("name").toString(); QString pluginName = pluginMetadata.value("name").toString();
loggingFiltersPlugins << pluginName.left(1).toUpper() + pluginName.mid(1); loggingFiltersPlugins << pluginName.at(0).toUpper() + pluginName.mid(1);
} }
return loggingFiltersPlugins; return loggingFiltersPlugins;
} }
@ -368,7 +369,6 @@ JsonRPCServerImplementation *NymeaCore::jsonRPCServer() const
void NymeaCore::thingManagerLoaded() void NymeaCore::thingManagerLoaded()
{ {
// Tell hardare resources we're done with loading stuff... // Tell hardare resources we're done with loading stuff...
m_hardwareManager->thingsLoaded(); m_hardwareManager->thingsLoaded();
@ -396,7 +396,6 @@ void NymeaCore::thingManagerLoaded()
m_tagsStorage->removeTag(tag); m_tagsStorage->removeTag(tag);
} }
} }
} }
} }

View File

@ -66,7 +66,6 @@ private:
QString m_displayName; QString m_displayName;
Types::PermissionScopes m_scopes = Types::PermissionScopeNone; Types::PermissionScopes m_scopes = Types::PermissionScopeNone;
QList<ThingId> m_allowedThingIds; QList<ThingId> m_allowedThingIds;
}; };
class UserInfoList: public QList<UserInfo> class UserInfoList: public QList<UserInfo>
@ -78,4 +77,7 @@ public:
Q_INVOKABLE void put(const QVariant &variant); Q_INVOKABLE void put(const QVariant &variant);
}; };
} }
Q_DECLARE_METATYPE(nymeaserver::UserInfo);
#endif // USERINFO_H #endif // USERINFO_H

View File

@ -63,7 +63,6 @@
*/ */
#include "usermanager.h" #include "usermanager.h"
#include "nymeasettings.h"
#include "loggingcategories.h" #include "loggingcategories.h"
#include "pushbuttondbusservice.h" #include "pushbuttondbusservice.h"
#include "nymeacore.h" #include "nymeacore.h"
@ -192,7 +191,8 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
return UserErrorDuplicateUserId; return UserErrorDuplicateUserId;
} }
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); static QRegularExpression bracketsRe("[{}]");
QByteArray salt = QUuid::createUuid().toString().remove(bracketsRe).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64(); QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QSqlQuery query(m_db); QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds)" query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds)"
@ -305,6 +305,34 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types
} }
} }
QList<ThingId> thingsAppeared;
QList<ThingId> thingsDisappeared;
// Get the current allowed things
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
// Restricted thing access, let's notify this user if any things appeared or dissapeard for the user
UserInfo currentUserInfo = userInfo(username);
// Get new appeared things for this user
foreach (const ThingId &thingId, thingIds) {
if (currentUserInfo.allowedThingIds().contains(thingId))
continue;
qCDebug(dcUserManager()) << "Thing with ID" << thingId.toString() << "now allowed for this user any more. Notify user" << username << "that thing appeared.";
thingsAppeared.append(thingId);
}
// Get disappeared things for this user
foreach (const ThingId &thingId, currentUserInfo.allowedThingIds()) {
if (thingIds.contains(thingId))
continue;
qCDebug(dcUserManager()) << "Thing with ID" << thingId.toString() << "not allowed for this user any more. Notify user" << username << "that thing dissappeared.";
thingsDisappeared.append(thingId);
}
}
QString scopesString = Types::scopesToStringList(scopes).join(','); QString scopesString = Types::scopesToStringList(scopes).join(',');
QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(','); QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(',');
@ -320,16 +348,25 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types
} }
emit userChanged(username); emit userChanged(username);
// Notify after updating the user information
UserInfo ui = userInfo(username);
foreach (const ThingId &thingId, thingsAppeared)
emit userThingRestrictionsChanged(ui, thingId, true);
foreach (const ThingId &thingId, thingsDisappeared)
emit userThingRestrictionsChanged(ui, thingId, false);
return UserErrorNoError; return UserErrorNoError;
} }
UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName) UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName)
{ {
QSqlQuery query(m_db); QSqlQuery query(m_db);
query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;"); query.prepare("UPDATE users SET email = :email, displayName = :displayName WHERE username = :username;");
query.addBindValue(email); query.bindValue(":email", email);
query.addBindValue(displayName); query.bindValue(":displayName", displayName);
query.addBindValue(username); query.bindValue(":username", username);
query.exec(); query.exec();
if (query.lastError().type() != QSqlError::NoError) { if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery(); qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
@ -454,7 +491,6 @@ UserInfo UserManager::userInfo(const QString &username) const
userInfo.setDisplayName(getUserQuery.value("displayName").toString()); userInfo.setDisplayName(getUserQuery.value("displayName").toString());
userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(','))); userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(',')));
userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(','))); userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(',')));
return userInfo; return userInfo;
} }
@ -575,6 +611,7 @@ bool UserManager::verifyToken(const QByteArray &token)
qCDebug(dcUserManager) << "Authorization failed for token" << token; qCDebug(dcUserManager) << "Authorization failed for token" << token;
return false; return false;
} }
//qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString(); //qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
return true; return true;
} }
@ -598,6 +635,23 @@ QList<ThingId> UserManager::getAllowedThingIdsForToken(const QByteArray &token)
return userInfo(tokenInfo(token).username()).allowedThingIds(); return userInfo(tokenInfo(token).username()).allowedThingIds();
} }
void UserManager::onThingRemoved(const ThingId &thingId)
{
// If a thing has been removed from the system, clean up any thing based permissions
foreach (const UserInfo &userInfo, users()) {
if (userInfo.allowedThingIds().contains(thingId)) {
QList<ThingId> allowedThingIds = userInfo.allowedThingIds();
allowedThingIds.removeAll(thingId);
if (setUserScopes(userInfo.username(), userInfo.scopes(), allowedThingIds) != UserErrorNoError) {
qCWarning(dcUserManager()) << "Failed to remove thing with ID" << thingId.toString() << "from allowed things of user" << userInfo.username();
} else {
qCDebug(dcUserManager()) << "Removed thing with ID" << thingId.toString() << "from allowed things of user" << userInfo.username();
}
}
}
}
bool UserManager::initDB() bool UserManager::initDB()
{ {
m_db.close(); m_db.close();
@ -775,35 +829,6 @@ bool UserManager::initDB()
} }
} }
// Migration from before 1.0:
// Push button tokens were given out without an explicit user name
// If we have push button tokens (userId "") but no explicit user, let's create it as admin
// Users without valid username will have password login disabled.
QSqlQuery query(m_db);
query.prepare("SELECT * FROM tokens WHERE username = \"\";");
query.exec();
if (query.lastError().type() == QSqlError::NoError && query.next()) {
QSqlQuery query(m_db);
query.prepare("SELECT * FROM users WHERE username = \"\";");
query.exec();
if (!query.next()) {
qCDebug(dcUserManager()) << "Tokens existing but no user. Creating token admin user";
QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) values(?, ?, ?, ?, ?, ?);");
query.addBindValue("");
query.addBindValue("");
query.addBindValue("Admin");
query.addBindValue("");
query.addBindValue("");
query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin));
query.exec();
if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText();
}
}
}
qCDebug(dcUserManager()) << "User database initialized successfully"; qCDebug(dcUserManager()) << "User database initialized successfully";
return true; return true;
} }
@ -811,9 +836,8 @@ bool UserManager::initDB()
void UserManager::rotate(const QString &dbName) void UserManager::rotate(const QString &dbName)
{ {
int index = 1; int index = 1;
while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index))) { while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index)))
index++; index++;
}
qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index); qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index);
QFile f(dbName); QFile f(dbName);
@ -826,30 +850,33 @@ void UserManager::rotate(const QString &dbName)
bool UserManager::validateUsername(const QString &username) const bool UserManager::validateUsername(const QString &username) const
{ {
QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}"); static QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}");
return validator.match(username).hasMatch(); return validator.match(username).hasMatch();
} }
bool UserManager::validatePassword(const QString &password) const bool UserManager::validatePassword(const QString &password) const
{ {
if (password.length() < 8) { if (password.length() < 8)
return false; return false;
}
if (!password.contains(QRegularExpression("[a-z]"))) { static QRegularExpression lowerRe("[a-z]");
if (!password.contains(lowerRe))
return false; return false;
}
if (!password.contains(QRegularExpression("[A-Z]"))) { static QRegularExpression upperRe("[A-Z]");
if (!password.contains(upperRe))
return false; return false;
}
if (!password.contains(QRegularExpression("[0-9]"))) { static QRegularExpression numbersRe("[0-9]");
if (!password.contains(numbersRe))
return false; return false;
}
return true; return true;
} }
bool UserManager::validateToken(const QByteArray &token) const bool UserManager::validateToken(const QByteArray &token) const
{ {
QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)")); static QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)"));
return validator.match(token).hasMatch(); return validator.match(token).hasMatch();
} }
@ -899,6 +926,11 @@ void UserManager::dumpDBError(const QString &message)
qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
} }
void UserManager::evaluateAllowedThingsForUser()
{
}
void UserManager::onPushButtonPressed() void UserManager::onPushButtonPressed()
{ {
if (m_pushButtonTransaction.first == -1) { if (m_pushButtonTransaction.first == -1) {

View File

@ -81,12 +81,17 @@ public:
bool accessToThingGranted(const ThingId &thingId, const QByteArray &token); bool accessToThingGranted(const ThingId &thingId, const QByteArray &token);
QList<ThingId> getAllowedThingIdsForToken(const QByteArray &token) const; QList<ThingId> getAllowedThingIdsForToken(const QByteArray &token) const;
public slots:
void onThingRemoved(const ThingId &thingId);
signals: signals:
void userAdded(const QString &username); void userAdded(const QString &username);
void userRemoved(const QString &username); void userRemoved(const QString &username);
void userChanged(const QString &username); void userChanged(const QString &username);
void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
void userThingRestrictionsChanged(const nymeaserver::UserInfo &userInfo, const ThingId &thingId, bool accessGranted);
private: private:
bool initDB(); bool initDB();
void rotate(const QString &dbName); void rotate(const QString &dbName);
@ -97,6 +102,8 @@ private:
void dumpDBError(const QString &message); void dumpDBError(const QString &message);
void evaluateAllowedThingsForUser();
private slots: private slots:
void onPushButtonPressed(); void onPushButtonPressed();

View File

@ -620,10 +620,10 @@ void TestUsermanager::testRestrictedThingAccess()
// Add thing two // Add thing two
QVariantMap httpportParamTwo; QVariantMap httpportParamTwo;
httpportParamOne.insert("paramTypeId", mockThingHttpportParamTypeId.toString()); httpportParamTwo.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
httpportParamOne.insert("value", m_mockThing1Port - 2); httpportParamTwo.insert("value", m_mockThing1Port - 2);
thingParams.clear(); thingParams.clear();
thingParams << httpportParamOne; thingParams << httpportParamTwo;
params.clear(); params.clear();
params.insert("thingClassId", mockThingClassId); params.insert("thingClassId", mockThingClassId);
@ -689,33 +689,32 @@ void TestUsermanager::testRestrictedThingAccess()
response = injectAndWait("Integrations.GetThings", params); response = injectAndWait("Integrations.GetThings", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// GetStateValue // GetStateValue (no access)
params.clear(); params.clear();
params.insert("thingId", thingIdOne); params.insert("thingId", thingIdOne);
params.insert("stateTypeId", mockConnectedStateTypeId); params.insert("stateTypeId", mockConnectedStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params); response = injectAndWait("Integrations.GetStateValue", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// BrowseThing // BrowseThing (no access)
params.clear(); params.clear();
params.insert("thingId", thingIdOne); params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.BrowseThing", params); response = injectAndWait("Integrations.BrowseThing", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// GetBrowserItem // GetBrowserItem (no access)
params.clear(); params.clear();
params.insert("thingId", thingIdOne); params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.GetBrowserItem", params); response = injectAndWait("Integrations.GetBrowserItem", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// Make sure notification get received from allowed thing
// Make sure no notification will be recived from restricted thing
// Clean up // Clean up
UserManager *userManager = NymeaCore::instance()->userManager();
foreach (const UserInfo &userInfo, userManager->users()) {
qCDebug(dcTests()) << "Removing user" << userInfo.username();
userManager->removeUser(userInfo.username());
}
userManager->removeUser("");
} }
QTEST_MAIN(TestUsermanager) QTEST_MAIN(TestUsermanager)