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){
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 &params, 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::ThingError>(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 &paramTypeId, 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

View File

@ -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 &params);
void StateChanged(const QVariantMap &params);
void ThingRemoved(const QVariantMap &params);
void ThingAdded(const QVariantMap &params);
void ThingChanged(const QVariantMap &params);
void ThingSettingChanged(const QVariantMap &params);
void EventTriggered(const QVariantMap &params);
// Thing permission relevant notifications
void StateChanged(const QVariantMap &params, const ThingId &thingId);
void ThingRemoved(const QVariantMap &params, const ThingId &thingId);
void ThingAdded(const QVariantMap &params, const ThingId &thingId);
void ThingChanged(const QVariantMap &params, const ThingId &thingId);
void ThingSettingChanged(const QVariantMap &params, const ThingId &thingId);
void EventTriggered(const QVariantMap &params, const ThingId &thingId);
void IOConnectionAdded(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:
void pluginConfigChanged(const PluginId &id, const ParamList &config);

View File

@ -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 &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()
{
JsonReply *reply = qobject_cast<JsonReply *>(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);
}

View File

@ -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 <QObject>
#include <QVariantMap>
#include <QString>
@ -90,6 +87,8 @@ private slots:
void sendNotification(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();

View File

@ -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);
}
}
}
}

View File

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

View File

@ -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<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 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<ThingId> 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<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()
{
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) {

View File

@ -81,12 +81,17 @@ public:
bool accessToThingGranted(const ThingId &thingId, const QByteArray &token);
QList<ThingId> 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();

View File

@ -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)