diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index d40e31ef..a26c24c2 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -446,7 +446,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){ QVariantMap params; params.insert("event", pack(event)); - emit EventTriggered(params); + emit EventTriggered(params, event.thingId()); }); params.clear(); returns.clear(); @@ -519,6 +519,21 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex(); 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 @@ -1143,8 +1158,7 @@ JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap ¶ms, cons IOConnections ioConnections = m_thingManager->ioConnections(thingId); QVariantMap returns; - QVariant bla = pack(ioConnections); - returns.insert("ioConnections", pack(ioConnections)); + returns.insert("ioConnections", pack(ioConnections)); returns.insert("thingError", enumValueName(Thing::ThingErrorNoError)); return createReply(returns); } @@ -1214,28 +1228,28 @@ void IntegrationsHandler::thingStateChanged(Thing *thing, const QUuid &stateType params.insert("minValue", minValue); params.insert("maxValue", maxValue); params.insert("possibleValues", possibleValues); - emit StateChanged(params); + emit StateChanged(params, thing->id()); } void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId) { QVariantMap params; params.insert("thingId", thingId); - emit ThingRemoved(params); + emit ThingRemoved(params, thingId); } void IntegrationsHandler::thingAddedNotification(Thing *thing) { QVariantMap params; params.insert("thing", pack(thing)); - emit ThingAdded(params); + emit ThingAdded(params, thing->id()); } void IntegrationsHandler::thingChangedNotification(Thing *thing) { QVariantMap params; params.insert("thing", pack(thing)); - emit ThingChanged(params); + emit ThingChanged(params, thing->id()); } void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId ¶mTypeId, const QVariant &value) @@ -1244,7 +1258,7 @@ void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId params.insert("thingId", thingId); params.insert("paramTypeId", paramTypeId.toString()); params.insert("value", value); - emit ThingSettingChanged(params); + emit ThingSettingChanged(params, thingId); } QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const diff --git a/libnymea-core/jsonrpc/integrationshandler.h b/libnymea-core/jsonrpc/integrationshandler.h index 56c56f9c..7c4c5c5a 100644 --- a/libnymea-core/jsonrpc/integrationshandler.h +++ b/libnymea-core/jsonrpc/integrationshandler.h @@ -26,6 +26,7 @@ #define INTEGRATIONSHANDLER_H #include "jsonrpc/jsonhandler.h" +#include "usermanager/userinfo.h" #include "integrations/thingmanager.h" namespace nymeaserver { @@ -80,15 +81,20 @@ public: signals: void PluginConfigurationChanged(const QVariantMap ¶ms); - void StateChanged(const QVariantMap ¶ms); - void ThingRemoved(const QVariantMap ¶ms); - void ThingAdded(const QVariantMap ¶ms); - void ThingChanged(const QVariantMap ¶ms); - void ThingSettingChanged(const QVariantMap ¶ms); - void EventTriggered(const QVariantMap ¶ms); + // Thing permission relevant notifications + void StateChanged(const QVariantMap ¶ms, const ThingId &thingId); + void ThingRemoved(const QVariantMap ¶ms, const ThingId &thingId); + void ThingAdded(const QVariantMap ¶ms, const ThingId &thingId); + void ThingChanged(const QVariantMap ¶ms, const ThingId &thingId); + void ThingSettingChanged(const QVariantMap ¶ms, const ThingId &thingId); + void EventTriggered(const QVariantMap ¶ms, const ThingId &thingId); void IOConnectionAdded(const QVariantMap ¶ms); void IOConnectionRemoved(const QVariantMap ¶ms); + // User specific notifications depending on the thing based permissions + void ThingRemoved(const QVariantMap ¶ms, const nymeaserver::UserInfo &userInfo); + void ThingAdded(const QVariantMap ¶ms, const nymeaserver::UserInfo &userInfo); + private slots: void pluginConfigChanged(const PluginId &id, const ParamList &config); diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp index 20eef40e..4d7d20bc 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp @@ -44,19 +44,11 @@ #include "jsonvalidator.h" #include "nymeacore.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 "platform/platform.h" #include "version.h" #include "integrationshandler.h" #include "ruleshandler.h" -#include "scriptshandler.h" #include "logginghandler.h" #include "configurationhandler.h" #include "networkmanagerhandler.h" @@ -104,19 +96,19 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration // Methods QString description; QVariantMap returns; QVariantMap params; 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 " - "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 " - "(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" - "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, " - "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 " - "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 " - "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."; + "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 " + "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 " + "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 " + "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 " + "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 " + "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 " + "information about the tokens validity and the user and permissions for the given token."; params.insert("o:locale", enumValueName(String)); returns.insert("server", enumValueName(String)); returns.insert("name", enumValueName(String)); @@ -152,13 +144,13 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.clear(); returns.clear(); description = "Enable/Disable notifications for this connections. Either \"enabled\" or """ - "\"namespaces\" needs to be given but not both of them. The boolean based " - "\"enabled\" parameter will enable/disable all notifications at once. If " - "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 " - "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 " - "one namespace has been enabled."; + "\"namespaces\" needs to be given but not both of them. The boolean based " + "\"enabled\" parameter will enable/disable all notifications at once. If " + "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 " + "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 " + "one namespace has been enabled."; params.insert("o:namespaces", enumValueName(StringList)); params.insert("d:o:enabled", enumValueName(Bool)); returns.insert("namespaces", enumValueName(StringList)); @@ -178,9 +170,9 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration 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."; + "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)); @@ -192,18 +184,18 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration 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."; + "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)); @@ -228,6 +220,8 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration connect(NymeaCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServerImplementation::onPushButtonAuthFinished); + + m_connectionLockdownTimer.setSingleShot(true); m_connectionLockdownTimer.setInterval(3000); } @@ -801,6 +795,69 @@ void JsonRPCServerImplementation::sendClientNotification(const QUuid &clientId, m_clientTransports.value(clientId)->sendData(clientId, data); } +void JsonRPCServerImplementation::sendClientNotification(const QVariantMap ¶ms, const ThingId &thingId) +{ + JsonHandler *handler = qobject_cast(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 ¶ms, 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() { JsonReply *reply = qobject_cast(sender()); @@ -980,9 +1037,17 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler) QMetaMethod method = handler->metaObject()->method(i); if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegularExpression("^[A-Z]"))) { 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)"))); } 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)"))); + } 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); interface->terminateClientConnection(clientId); }); + m_newConnectionWaitTimers.insert(clientId, timer); timer->start(10000); } @@ -1024,9 +1090,11 @@ void JsonRPCServerImplementation::clientDisconnected(const QUuid &clientId) m_clientBuffers.remove(clientId); m_clientLocales.remove(clientId); m_clientTokens.remove(clientId); + if (m_pushButtonTransactions.values().contains(clientId)) { NymeaCore::instance()->userManager()->cancelPushButtonAuth(m_pushButtonTransactions.key(clientId)); } + if (m_newConnectionWaitTimers.contains(clientId)) { delete m_newConnectionWaitTimers.take(clientId); } diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h index d3c60737..85f7a1f4 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h @@ -27,12 +27,9 @@ #include "jsonrpc/jsonrpcserver.h" #include "jsonrpc/jsonhandler.h" +#include "usermanager/userinfo.h" #include "transportinterface.h" -#include "types/thingclass.h" -#include "types/action.h" -#include "types/event.h" - #include #include #include @@ -90,6 +87,8 @@ private slots: void sendNotification(const QVariantMap ¶ms); void sendClientNotification(const QUuid &clientId, const QVariantMap ¶ms); + void sendClientNotification(const QVariantMap ¶ms, const ThingId &thingId); + void sendClientNotification(const QVariantMap ¶ms, const nymeaserver::UserInfo &userInfo); void asyncReplyFinished(); diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index b0a59f0b..0872fd4a 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -160,10 +160,11 @@ void NymeaCore::init(const QStringList &additionalInterfaces, bool disableLogEng m_experienceManager = new ExperienceManager(m_thingManager, m_serverManager->jsonServer(), this); connect(m_configuration, &NymeaConfiguration::serverNameChanged, m_serverManager, &ServerManager::setServerName); - 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}}); + #ifdef WITH_SYSTEMD sd_notify(0, "READY=1"); #endif @@ -296,7 +297,7 @@ QStringList NymeaCore::loggingFiltersPlugins() QStringList loggingFiltersPlugins; foreach (const QJsonObject &pluginMetadata, ThingManagerImplementation::pluginsMetadata()) { QString pluginName = pluginMetadata.value("name").toString(); - loggingFiltersPlugins << pluginName.left(1).toUpper() + pluginName.mid(1); + loggingFiltersPlugins << pluginName.at(0).toUpper() + pluginName.mid(1); } return loggingFiltersPlugins; } @@ -368,7 +369,6 @@ JsonRPCServerImplementation *NymeaCore::jsonRPCServer() const void NymeaCore::thingManagerLoaded() { - // Tell hardare resources we're done with loading stuff... m_hardwareManager->thingsLoaded(); @@ -396,7 +396,6 @@ void NymeaCore::thingManagerLoaded() m_tagsStorage->removeTag(tag); } } - } } diff --git a/libnymea-core/usermanager/userinfo.h b/libnymea-core/usermanager/userinfo.h index 770b9bb5..98faab86 100644 --- a/libnymea-core/usermanager/userinfo.h +++ b/libnymea-core/usermanager/userinfo.h @@ -66,7 +66,6 @@ private: QString m_displayName; Types::PermissionScopes m_scopes = Types::PermissionScopeNone; QList m_allowedThingIds; - }; class UserInfoList: public QList @@ -78,4 +77,7 @@ public: Q_INVOKABLE void put(const QVariant &variant); }; } + +Q_DECLARE_METATYPE(nymeaserver::UserInfo); + #endif // USERINFO_H diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index ac793e15..aecab7da 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -63,7 +63,6 @@ */ #include "usermanager.h" -#include "nymeasettings.h" #include "loggingcategories.h" #include "pushbuttondbusservice.h" #include "nymeacore.h" @@ -192,7 +191,8 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS 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(); QSqlQuery query(m_db); 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 thingsAppeared; + QList 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 allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(','); @@ -320,16 +348,25 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types } 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; } UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName) { QSqlQuery query(m_db); - query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;"); - query.addBindValue(email); - query.addBindValue(displayName); - query.addBindValue(username); + query.prepare("UPDATE users SET email = :email, displayName = :displayName WHERE username = :username;"); + query.bindValue(":email", email); + query.bindValue(":displayName", displayName); + query.bindValue(":username", username); query.exec(); if (query.lastError().type() != QSqlError::NoError) { 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.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(','))); userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(','))); - return userInfo; } @@ -575,6 +611,7 @@ bool UserManager::verifyToken(const QByteArray &token) qCDebug(dcUserManager) << "Authorization failed for token" << token; return false; } + //qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString(); return true; } @@ -598,6 +635,23 @@ QList UserManager::getAllowedThingIdsForToken(const QByteArray &token) 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 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() { 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"; return true; } @@ -811,9 +836,8 @@ bool UserManager::initDB() void UserManager::rotate(const QString &dbName) { int index = 1; - while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index))) { + while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index))) index++; - } qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index); QFile f(dbName); @@ -826,30 +850,33 @@ void UserManager::rotate(const QString &dbName) 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(); } bool UserManager::validatePassword(const QString &password) const { - if (password.length() < 8) { + if (password.length() < 8) return false; - } - if (!password.contains(QRegularExpression("[a-z]"))) { + + static QRegularExpression lowerRe("[a-z]"); + if (!password.contains(lowerRe)) return false; - } - if (!password.contains(QRegularExpression("[A-Z]"))) { + + static QRegularExpression upperRe("[A-Z]"); + if (!password.contains(upperRe)) return false; - } - if (!password.contains(QRegularExpression("[0-9]"))) { + + static QRegularExpression numbersRe("[0-9]"); + if (!password.contains(numbersRe)) return false; - } + return true; } 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(); } @@ -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(); } +void UserManager::evaluateAllowedThingsForUser() +{ + +} + void UserManager::onPushButtonPressed() { if (m_pushButtonTransaction.first == -1) { diff --git a/libnymea-core/usermanager/usermanager.h b/libnymea-core/usermanager/usermanager.h index 5ee0bf0c..dc741a67 100644 --- a/libnymea-core/usermanager/usermanager.h +++ b/libnymea-core/usermanager/usermanager.h @@ -81,12 +81,17 @@ public: bool accessToThingGranted(const ThingId &thingId, const QByteArray &token); QList getAllowedThingIdsForToken(const QByteArray &token) const; +public slots: + void onThingRemoved(const ThingId &thingId); + signals: void userAdded(const QString &username); void userRemoved(const QString &username); void userChanged(const QString &username); void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); + void userThingRestrictionsChanged(const nymeaserver::UserInfo &userInfo, const ThingId &thingId, bool accessGranted); + private: bool initDB(); void rotate(const QString &dbName); @@ -97,6 +102,8 @@ private: void dumpDBError(const QString &message); + void evaluateAllowedThingsForUser(); + private slots: void onPushButtonPressed(); diff --git a/tests/auto/usermanager/testusermanager.cpp b/tests/auto/usermanager/testusermanager.cpp index 6f9bd92b..00bb2c73 100644 --- a/tests/auto/usermanager/testusermanager.cpp +++ b/tests/auto/usermanager/testusermanager.cpp @@ -620,10 +620,10 @@ void TestUsermanager::testRestrictedThingAccess() // Add thing two QVariantMap httpportParamTwo; - httpportParamOne.insert("paramTypeId", mockThingHttpportParamTypeId.toString()); - httpportParamOne.insert("value", m_mockThing1Port - 2); + httpportParamTwo.insert("paramTypeId", mockThingHttpportParamTypeId.toString()); + httpportParamTwo.insert("value", m_mockThing1Port - 2); thingParams.clear(); - thingParams << httpportParamOne; + thingParams << httpportParamTwo; params.clear(); params.insert("thingClassId", mockThingClassId); @@ -689,33 +689,32 @@ void TestUsermanager::testRestrictedThingAccess() response = injectAndWait("Integrations.GetThings", params); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); - // GetStateValue + // GetStateValue (no access) params.clear(); params.insert("thingId", thingIdOne); params.insert("stateTypeId", mockConnectedStateTypeId); response = injectAndWait("Integrations.GetStateValue", params); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); - // BrowseThing + // BrowseThing (no access) params.clear(); params.insert("thingId", thingIdOne); response = injectAndWait("Integrations.BrowseThing", params); verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound)); - // GetBrowserItem + // GetBrowserItem (no access) params.clear(); params.insert("thingId", thingIdOne); response = injectAndWait("Integrations.GetBrowserItem", params); 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 - - + 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)