Add thing added and removed logic depending on users thing permission
This commit is contained in:
parent
b80ad6d839
commit
360e287619
@ -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::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 ¶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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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<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 ¶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<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);
|
||||
}
|
||||
|
||||
@ -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 ¶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();
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user