Merge PR #711: Thing based user permissions

This commit is contained in:
jenkins 2026-01-19 10:09:45 +01:00
commit 6cf709d33d
20 changed files with 1342 additions and 471 deletions

View File

@ -38,6 +38,9 @@
#include "integrations/browseritemresult.h" #include "integrations/browseritemresult.h"
#include "ruleengine/ruleengine.h" #include "ruleengine/ruleengine.h"
#include "nymeacore.h"
#include "usermanager/usermanager.h"
#include <QDebug> #include <QDebug>
#include <QJsonDocument> #include <QJsonDocument>
#include <QCryptographicHash> #include <QCryptographicHash>
@ -128,12 +131,12 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Add a new thing to the system. " description = "Add a new thing to the system. "
"Only things with a setupMethod of SetupMethodJustAdd can be added this way. " "Only things with a setupMethod of SetupMethodJustAdd can be added this way. "
"For things with a setupMethod different than SetupMethodJustAdd, use PairThing. " "For things with a setupMethod different than SetupMethodJustAdd, use PairThing. "
"Things with CreateMethodJustAdd require all parameters to be supplied here. " "Things with CreateMethodJustAdd require all parameters to be supplied here. "
"Things with CreateMethodDiscovery require the use of a thingDescriptorId. For discovered " "Things with CreateMethodDiscovery require the use of a thingDescriptorId. For discovered "
"things, params are not required and will be taken from the ThingDescriptor, however, they " "things, params are not required and will be taken from the ThingDescriptor, however, they "
"may be overridden by supplying thingParams."; "may be overridden by supplying thingParams.";
params.insert("o:thingClassId", enumValueName(Uuid)); params.insert("o:thingClassId", enumValueName(Uuid));
params.insert("name", enumValueName(String)); params.insert("name", enumValueName(String));
params.insert("o:thingDescriptorId", enumValueName(Uuid)); params.insert("o:thingDescriptorId", enumValueName(Uuid));
@ -145,23 +148,23 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Pair a new thing. " description = "Pair a new thing. "
"Use this to set up or reconfigure things for ThingClasses with a setupMethod different than SetupMethodJustAdd. " "Use this to set up or reconfigure things for ThingClasses with a setupMethod different than SetupMethodJustAdd. "
"Depending on the CreateMethod and whether a new thing is set up or an existing one is reconfigured, different parameters " "Depending on the CreateMethod and whether a new thing is set up or an existing one is reconfigured, different parameters "
"are required:\n" "are required:\n"
"CreateMethodJustAdd takes the thingClassId and the parameters you want to have with that thing. " "CreateMethodJustAdd takes the thingClassId and the parameters you want to have with that thing. "
"If an existing thing should be reconfigured, the thingId of said thing should be given additionally.\n" "If an existing thing should be reconfigured, the thingId of said thing should be given additionally.\n"
"CreateMethodDiscovery requires the use of a thingDescriptorId, previously obtained with DiscoverThings. Optionally, " "CreateMethodDiscovery requires the use of a thingDescriptorId, previously obtained with DiscoverThings. Optionally, "
"parameters can be overridden with the give thingParams. ThingDescriptors containing a thingId will reconfigure an " "parameters can be overridden with the give thingParams. ThingDescriptors containing a thingId will reconfigure an "
"existing thing, descriptors without a thingId will add a new thing to the system.\n" "existing thing, descriptors without a thingId will add a new thing to the system.\n"
"If success is true, the return values will contain a pairingTransactionId, a displayMessage and " "If success is true, the return values will contain a pairingTransactionId, a displayMessage and "
"the setupMethod. Depending on the setupMethod, the application should present the use an appropriate login mask, " "the setupMethod. Depending on the setupMethod, the application should present the use an appropriate login mask, "
"that is, For SetupMethodDisplayPin the user should enter a pin that is displayed on the device or online service, for SetupMethodEnterPin the " "that is, For SetupMethodDisplayPin the user should enter a pin that is displayed on the device or online service, for SetupMethodEnterPin the "
"application should present the given PIN so the user can enter it on the device or online service. For SetupMethodPushButton, the displayMessage " "application should present the given PIN so the user can enter it on the device or online service. For SetupMethodPushButton, the displayMessage "
"shall be presented to the user as informational hints to press a button on the device. For SetupMethodUserAndPassword a login " "shall be presented to the user as informational hints to press a button on the device. For SetupMethodUserAndPassword a login "
"mask for a user and password login should be presented to the user. In case of SetupMethodOAuth, an OAuth URL will be returned " "mask for a user and password login should be presented to the user. In case of SetupMethodOAuth, an OAuth URL will be returned "
"which shall be opened in a web view to allow the user logging in.\n" "which shall be opened in a web view to allow the user logging in.\n"
"Once the login procedure has completed, the application shall proceed with ConfirmPairing, providing the results of the pairing " "Once the login procedure has completed, the application shall proceed with ConfirmPairing, providing the results of the pairing "
"procedure."; "procedure.";
params.insert("o:thingClassId", enumValueName(Uuid)); params.insert("o:thingClassId", enumValueName(Uuid));
params.insert("o:name", enumValueName(String)); params.insert("o:name", enumValueName(String));
params.insert("o:thingDescriptorId", enumValueName(Uuid)); params.insert("o:thingDescriptorId", enumValueName(Uuid));
@ -177,9 +180,9 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Confirm an ongoing pairing. For SetupMethodUserAndPassword, provide the username in the \"username\" field " description = "Confirm an ongoing pairing. For SetupMethodUserAndPassword, provide the username in the \"username\" field "
"and the password in the \"secret\" field. For SetupMethodEnterPin and provide the PIN in the \"secret\" " "and the password in the \"secret\" field. For SetupMethodEnterPin and provide the PIN in the \"secret\" "
"field. In case of SetupMethodOAuth, the previously opened web view will eventually be redirected to http://128.0.0.1:8888 " "field. In case of SetupMethodOAuth, the previously opened web view will eventually be redirected to http://128.0.0.1:8888 "
"and the OAuth code as query parameters to this url. Provide the entire unmodified URL in the secret field."; "and the OAuth code as query parameters to this url. Provide the entire unmodified URL in the secret field.";
params.insert("pairingTransactionId", enumValueName(Uuid)); params.insert("pairingTransactionId", enumValueName(Uuid));
params.insert("o:username", enumValueName(String)); params.insert("o:username", enumValueName(String));
params.insert("o:secret", enumValueName(String)); params.insert("o:secret", enumValueName(String));
@ -197,10 +200,10 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Performs a thing discovery for things of the given thingClassId and returns the results. " description = "Performs a thing discovery for things of the given thingClassId and returns the results. "
"This function may take a while to return. Note that this method will include all the found " "This function may take a while to return. Note that this method will include all the found "
"things, that is, including things that may already have been added. Those things will have " "things, that is, including things that may already have been added. Those things will have "
"thingId set to the id of the already added thing. Such results may be used to reconfigure " "thingId set to the id of the already added thing. Such results may be used to reconfigure "
"existing things and might be filtered in cases where only unknown things are of interest."; "existing things and might be filtered in cases where only unknown things are of interest.";
params.insert("thingClassId", enumValueName(Uuid)); params.insert("thingClassId", enumValueName(Uuid));
params.insert("o:discoveryParams", objectRef<ParamList>()); params.insert("o:discoveryParams", objectRef<ParamList>());
returns.insert("thingError", enumRef<Thing::ThingError>()); returns.insert("thingError", enumRef<Thing::ThingError>());
@ -311,26 +314,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Browse a thing. " description = "Browse a thing. "
"If a ThingClass indicates a thing is browsable, this method will return the BrowserItems. If no " "If a ThingClass indicates a thing is browsable, this method will return the BrowserItems. If no "
"parameter besides the thingId is used, the root node of this thingwill be returned. Any " "parameter besides the thingId is used, the root node of this thingwill be returned. Any "
"returned item which is browsable can be passed as node. Results will be children of the given node.\n" "returned item which is browsable can be passed as node. Results will be children of the given node.\n"
"In case of an error during browsing, the error will be indicated and the displayMessage may contain " "In case of an error during browsing, the error will be indicated and the displayMessage may contain "
"additional information for the user. The displayMessage will be translated. A client UI showing this " "additional information for the user. The displayMessage will be translated. A client UI showing this "
"message to the user should be prepared for empty, but also longer strings."; "message to the user should be prepared for empty, but also longer strings.";
params.insert("thingId", enumValueName(Uuid)); params.insert("thingId", enumValueName(Uuid));
params.insert("o:itemId", enumValueName(String)); params.insert("o:itemId", enumValueName(String));
returns.insert("thingError", enumRef<Thing::ThingError>()); returns.insert("thingError", enumRef<Thing::ThingError>());
returns.insert("o:displayMessage", enumValueName(String)); returns.insert("o:displayMessage", enumValueName(String));
returns.insert("items", QVariantList() << objectRef("BrowserItem")); returns.insert("o:items", QVariantList() << objectRef("BrowserItem"));
registerMethod("BrowseThing", description, params, returns, Types::PermissionScopeNone); registerMethod("BrowseThing", description, params, returns, Types::PermissionScopeNone);
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Get a single item from the browser. " description = "Get a single item from the browser. "
"This won't give any more info on an item than a regular BrowseThing call, but it allows to fetch " "This won't give any more info on an item than a regular BrowseThing call, but it allows to fetch "
"details of an item if only the ID is known.\n" "details of an item if only the ID is known.\n"
"In case of an error during browsing, the error will be indicated and the displayMessage may contain " "In case of an error during browsing, the error will be indicated and the displayMessage may contain "
"additional information for the user. The displayMessage will be translated. A client UI showing this " "additional information for the user. The displayMessage will be translated. A client UI showing this "
"message to the user should be prepared for empty, but also longer strings."; "message to the user should be prepared for empty, but also longer strings.";
params.insert("thingId", enumValueName(Uuid)); params.insert("thingId", enumValueName(Uuid));
params.insert("o:itemId", enumValueName(String)); params.insert("o:itemId", enumValueName(String));
returns.insert("thingError", enumRef<Thing::ThingError>()); returns.insert("thingError", enumRef<Thing::ThingError>());
@ -374,7 +377,8 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId."; description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId.";
params.insert("o:thingId", enumValueName(Uuid)); params.insert("o:thingId", enumValueName(Uuid));
returns.insert("ioConnections", objectRef<IOConnections>()); returns.insert("o:ioConnections", objectRef<IOConnections>());
returns.insert("thingError", enumRef<Thing::ThingError>());
registerMethod("GetIOConnections", description, params, returns, Types::PermissionScopeNone); registerMethod("GetIOConnections", description, params, returns, Types::PermissionScopeNone);
params.clear(); returns.clear(); params.clear(); returns.clear();
@ -442,7 +446,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){ connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){
QVariantMap params; QVariantMap params;
params.insert("event", pack(event)); params.insert("event", pack(event));
emit EventTriggered(params); emit EventTriggered(params, event.thingId());
}); });
params.clear(); returns.clear(); params.clear(); returns.clear();
@ -515,6 +519,21 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex(); hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex();
m_cacheHashes.insert("GetPlugins", hash); m_cacheHashes.insert("GetPlugins", hash);
}); });
connect(NymeaCore::instance()->userManager(), &UserManager::userThingRestrictionsChanged, this, [this](const UserInfo &userInfo, const ThingId &thingId, bool accessGranted){
if (accessGranted) {
QVariantMap params;
params.insert("thing", pack(m_thingManager->findConfiguredThing(thingId)));
emit ThingAdded(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been granted.";
} else {
QVariantMap params;
params.insert("thingId", thingId);
emit ThingRemoved(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been dropped.";
}
});
} }
QString IntegrationsHandler::name() const QString IntegrationsHandler::name() const
@ -527,7 +546,7 @@ QHash<QString, QString> IntegrationsHandler::cacheHashes() const
return m_cacheHashes; return m_cacheHashes;
} }
JsonReply* IntegrationsHandler::GetVendors(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetVendors(const QVariantMap &params, const JsonContext &context) const
{ {
Q_UNUSED(params) Q_UNUSED(params)
QVariantList vendors; QVariantList vendors;
@ -541,7 +560,7 @@ JsonReply* IntegrationsHandler::GetVendors(const QVariantMap &params, const Json
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::GetThingClasses(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetThingClasses(const QVariantMap &params, const JsonContext &context) const
{ {
QVariantMap returns; QVariantMap returns;
QVariantList thingClasses; QVariantList thingClasses;
@ -600,13 +619,13 @@ JsonReply *IntegrationsHandler::DiscoverThings(const QVariantMap &params, const
} }
reply->setData(returns); reply->setData(returns);
reply->finished(); emit reply->finished();
}); });
return reply; return reply;
} }
JsonReply* IntegrationsHandler::GetPlugins(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetPlugins(const QVariantMap &params, const JsonContext &context) const
{ {
Q_UNUSED(params) Q_UNUSED(params)
QVariantList plugins; QVariantList plugins;
@ -640,7 +659,7 @@ JsonReply *IntegrationsHandler::GetPluginConfiguration(const QVariantMap &params
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params) JsonReply *IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params)
{ {
QVariantMap returns; QVariantMap returns;
PluginId pluginId = PluginId(params.value("pluginId").toString()); PluginId pluginId = PluginId(params.value("pluginId").toString());
@ -650,7 +669,7 @@ JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::AddThing(const QVariantMap &params, const JsonContext &context) JsonReply *IntegrationsHandler::AddThing(const QVariantMap &params, const JsonContext &context)
{ {
ThingClassId thingClassId(params.value("thingClassId").toString()); ThingClassId thingClassId(params.value("thingClassId").toString());
QString thingName = params.value("name").toString(); QString thingName = params.value("name").toString();
@ -667,7 +686,7 @@ JsonReply* IntegrationsHandler::AddThing(const QVariantMap &params, const JsonCo
QVariantMap returns; QVariantMap returns;
returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorMissingParameter)); returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorMissingParameter));
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
return jsonReply; return jsonReply;
} }
info = m_thingManager->addConfiguredThing(thingClassId, thingParams, thingName); info = m_thingManager->addConfiguredThing(thingClassId, thingParams, thingName);
@ -687,7 +706,7 @@ JsonReply* IntegrationsHandler::AddThing(const QVariantMap &params, const JsonCo
returns.insert("thingId", info->thing()->id()); returns.insert("thingId", info->thing()->id());
} }
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -732,7 +751,7 @@ JsonReply *IntegrationsHandler::PairThing(const QVariantMap &params, const JsonC
} }
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -759,39 +778,74 @@ JsonReply *IntegrationsHandler::ConfirmPairing(const QVariantMap &params)
returns.insert("thingId", info->thingId().toString()); returns.insert("thingId", info->thingId().toString());
} }
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
} }
JsonReply* IntegrationsHandler::GetThings(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetThings(const QVariantMap &params, const JsonContext &context) const
{ {
QVariantMap returns; QVariantMap returns;
QVariantList things; QVariantList things;
if (params.contains("thingId")) {
Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString())); if (NymeaCore::instance()->userManager()->hasRestrictedThingAccess(context.token())) {
if (!thing) { // Restricted things access
returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorThingNotFound)); QList<ThingId> allowedThingIds = NymeaCore::instance()->userManager()->getAllowedThingIdsForToken(context.token());
return createReply(returns); if (params.contains("thingId")) {
} else { ThingId thingId(params.value("thingId").toString());
QVariantMap packedThing = pack(thing).toMap(); Thing *thing = m_thingManager->findConfiguredThing(thingId);
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale()); if (!thing || !allowedThingIds.contains(thingId)) {
if (!translatedSetupStatus.isEmpty()) { returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorThingNotFound));
packedThing["setupDisplayMessage"] = translatedSetupStatus; return createReply(returns);
} else {
QVariantMap packedThing = pack(thing).toMap();
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale());
if (!translatedSetupStatus.isEmpty()) {
packedThing["setupDisplayMessage"] = translatedSetupStatus;
}
things.append(packedThing);
}
} else {
foreach (Thing *thing, m_thingManager->configuredThings()) {
if (!allowedThingIds.contains(thing->id()))
continue;
QVariantMap packedThing = pack(thing).toMap();
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale());
if (!translatedSetupStatus.isEmpty()) {
packedThing["setupDisplayMessage"] = translatedSetupStatus;
}
things.append(packedThing);
} }
things.append(packedThing);
} }
} else { } else {
foreach (Thing *thing, m_thingManager->configuredThings()) { // Unrestricted things access
QVariantMap packedThing = pack(thing).toMap(); if (params.contains("thingId")) {
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale()); Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString()));
if (!translatedSetupStatus.isEmpty()) { if (!thing) {
packedThing["setupDisplayMessage"] = translatedSetupStatus; returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorThingNotFound));
return createReply(returns);
} else {
QVariantMap packedThing = pack(thing).toMap();
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale());
if (!translatedSetupStatus.isEmpty()) {
packedThing["setupDisplayMessage"] = translatedSetupStatus;
}
things.append(packedThing);
}
} else {
foreach (Thing *thing, m_thingManager->configuredThings()) {
QVariantMap packedThing = pack(thing).toMap();
QString translatedSetupStatus = m_thingManager->translate(thing->pluginId(), thing->setupDisplayMessage(), context.locale());
if (!translatedSetupStatus.isEmpty()) {
packedThing["setupDisplayMessage"] = translatedSetupStatus;
}
things.append(packedThing);
} }
things.append(packedThing);
} }
} }
returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorNoError)); returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorNoError));
returns.insert("things", things); returns.insert("things", things);
return createReply(returns); return createReply(returns);
@ -819,13 +873,11 @@ JsonReply *IntegrationsHandler::ReconfigureThing(const QVariantMap &params, cons
} }
connect(info, &ThingSetupInfo::finished, jsonReply, [info, jsonReply, locale](){ connect(info, &ThingSetupInfo::finished, jsonReply, [info, jsonReply, locale](){
QVariantMap returns; QVariantMap returns;
returns.insert("thingError", enumValueName<Thing::ThingError>(info->status())); returns.insert("thingError", enumValueName<Thing::ThingError>(info->status()));
returns.insert("displayMessage", info->translatedDisplayMessage(locale)); returns.insert("displayMessage", info->translatedDisplayMessage(locale));
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -843,7 +895,7 @@ JsonReply *IntegrationsHandler::EditThing(const QVariantMap &params)
return createReply(statusToReply(status)); return createReply(statusToReply(status));
} }
JsonReply* IntegrationsHandler::RemoveThing(const QVariantMap &params) JsonReply *IntegrationsHandler::RemoveThing(const QVariantMap &params)
{ {
QVariantMap returns; QVariantMap returns;
ThingId thingId = ThingId(params.value("thingId").toString()); ThingId thingId = ThingId(params.value("thingId").toString());
@ -898,7 +950,7 @@ JsonReply *IntegrationsHandler::SetStateFilter(const QVariantMap &params)
return createReply(statusToReply(status)); return createReply(statusToReply(status));
} }
JsonReply* IntegrationsHandler::GetEventTypes(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetEventTypes(const QVariantMap &params, const JsonContext &context) const
{ {
ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString())); ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString()));
ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale()); ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -908,7 +960,7 @@ JsonReply* IntegrationsHandler::GetEventTypes(const QVariantMap &params, const J
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::GetActionTypes(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetActionTypes(const QVariantMap &params, const JsonContext &context) const
{ {
ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString())); ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString()));
ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale()); ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -918,7 +970,7 @@ JsonReply* IntegrationsHandler::GetActionTypes(const QVariantMap &params, const
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::GetStateTypes(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetStateTypes(const QVariantMap &params, const JsonContext &context) const
{ {
ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString())); ThingClass thingClass = m_thingManager->findThingClass(ThingClassId(params.value("thingClassId").toString()));
ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale()); ThingClass translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -928,28 +980,34 @@ JsonReply* IntegrationsHandler::GetStateTypes(const QVariantMap &params, const J
return createReply(returns); return createReply(returns);
} }
JsonReply* IntegrationsHandler::GetStateValue(const QVariantMap &params) const JsonReply *IntegrationsHandler::GetStateValue(const QVariantMap &params, const JsonContext &context) const
{ {
Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString())); ThingId thingId(params.value("thingId").toString());
if (!thing) { if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound)); return createReply(statusToReply(Thing::ThingErrorThingNotFound));
}
Thing *thing = m_thingManager->findConfiguredThing(thingId);
if (!thing)
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
StateTypeId stateTypeId = StateTypeId(params.value("stateTypeId").toString()); StateTypeId stateTypeId = StateTypeId(params.value("stateTypeId").toString());
if (!thing->hasState(stateTypeId)) { if (!thing->hasState(stateTypeId))
return createReply(statusToReply(Thing::ThingErrorStateTypeNotFound)); return createReply(statusToReply(Thing::ThingErrorStateTypeNotFound));
}
QVariantMap returns = statusToReply(Thing::ThingErrorNoError); QVariantMap returns = statusToReply(Thing::ThingErrorNoError);
returns.insert("value", thing->state(stateTypeId).value()); returns.insert("value", thing->state(stateTypeId).value());
return createReply(returns); return createReply(returns);
} }
JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap &params) const JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap &params, const JsonContext &context) const
{ {
Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString())); ThingId thingId(params.value("thingId").toString());
if (!thing) { if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
Thing *thing = m_thingManager->findConfiguredThing(thingId);
if (!thing)
return createReply(statusToReply(Thing::ThingErrorThingNotFound)); return createReply(statusToReply(Thing::ThingErrorThingNotFound));
}
QVariantMap returns = statusToReply(Thing::ThingErrorNoError); QVariantMap returns = statusToReply(Thing::ThingErrorNoError);
returns.insert("values", pack(thing->states())); returns.insert("values", pack(thing->states()));
@ -958,7 +1016,10 @@ JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap &params) const
JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const JsonContext &context) const
{ {
ThingId thingId = ThingId(params.value("thingId").toString()); ThingId thingId(params.value("thingId").toString());
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
QString itemId = params.value("itemId").toString(); QString itemId = params.value("itemId").toString();
JsonReply *jsonReply = createAsyncReply("BrowseThing"); JsonReply *jsonReply = createAsyncReply("BrowseThing");
@ -968,15 +1029,16 @@ JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const Jso
QVariantMap returns = statusToReply(result->status()); QVariantMap returns = statusToReply(result->status());
QVariantList list; QVariantList list;
foreach (const BrowserItem &item, result->items()) { foreach (const BrowserItem &item, result->items())
list.append(packBrowserItem(item)); list.append(packBrowserItem(item));
}
returns.insert("items", list); returns.insert("items", list);
if (!result->displayMessage().isEmpty()) {
if (!result->displayMessage().isEmpty())
returns.insert("displayMessage", result->translatedDisplayMessage(context.locale())); returns.insert("displayMessage", result->translatedDisplayMessage(context.locale()));
}
jsonReply->setData(returns); jsonReply->setData(returns);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -984,10 +1046,11 @@ JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const Jso
JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const JsonContext &context) const JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const JsonContext &context) const
{ {
QVariantMap returns; ThingId thingId(params.value("thingId").toString());
ThingId thingId = ThingId(params.value("thingId").toString()); if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
QString itemId = params.value("itemId").toString(); return createReply(statusToReply(Thing::ThingErrorThingNotFound));
QString itemId = params.value("itemId").toString();
JsonReply *jsonReply = createAsyncReply("GetBrowserItem"); JsonReply *jsonReply = createAsyncReply("GetBrowserItem");
BrowserItemResult *result = m_thingManager->browserItemDetails(thingId, itemId, context.locale()); BrowserItemResult *result = m_thingManager->browserItemDetails(thingId, itemId, context.locale());
@ -1000,7 +1063,7 @@ JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const
params.insert("displayMessage", result->translatedDisplayMessage(context.locale())); params.insert("displayMessage", result->translatedDisplayMessage(context.locale()));
} }
jsonReply->setData(params); jsonReply->setData(params);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -1009,6 +1072,9 @@ JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const
JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const JsonContext &context) JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const JsonContext &context)
{ {
ThingId thingId(params.value("thingId").toString()); ThingId thingId(params.value("thingId").toString());
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
ActionTypeId actionTypeId(params.value("actionTypeId").toString()); ActionTypeId actionTypeId(params.value("actionTypeId").toString());
ParamList actionParams = unpack<ParamList>(params.value("params")); ParamList actionParams = unpack<ParamList>(params.value("params"));
QLocale locale = context.locale(); QLocale locale = context.locale();
@ -1026,7 +1092,7 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const J
data.insert("displayMessage", info->translatedDisplayMessage(locale)); data.insert("displayMessage", info->translatedDisplayMessage(locale));
} }
jsonReply->setData(data); jsonReply->setData(data);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -1035,6 +1101,9 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const J
JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context) JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context)
{ {
ThingId thingId = ThingId(params.value("thingId").toString()); ThingId thingId = ThingId(params.value("thingId").toString());
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
QString itemId = params.value("itemId").toString(); QString itemId = params.value("itemId").toString();
BrowserAction action(thingId, itemId); BrowserAction action(thingId, itemId);
@ -1048,7 +1117,7 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, co
data.insert("displayMessage", info->translatedDisplayMessage(context.locale())); data.insert("displayMessage", info->translatedDisplayMessage(context.locale()));
} }
jsonReply->setData(data); jsonReply->setData(data);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
@ -1057,6 +1126,9 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, co
JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context) JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context)
{ {
ThingId thingId = ThingId(params.value("thingId").toString()); ThingId thingId = ThingId(params.value("thingId").toString());
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
QString itemId = params.value("itemId").toString(); QString itemId = params.value("itemId").toString();
ActionTypeId actionTypeId = ActionTypeId(params.value("actionTypeId").toString()); ActionTypeId actionTypeId = ActionTypeId(params.value("actionTypeId").toString());
ParamList paramList = unpack<ParamList>(params.value("params")); ParamList paramList = unpack<ParamList>(params.value("params"));
@ -1072,19 +1144,22 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &para
data.insert("displayMessage", info->translatedDisplayMessage(context.locale())); data.insert("displayMessage", info->translatedDisplayMessage(context.locale()));
} }
jsonReply->setData(data); jsonReply->setData(data);
jsonReply->finished(); emit jsonReply->finished();
}); });
return jsonReply; return jsonReply;
} }
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params) JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params, const JsonContext &context)
{ {
ThingId thingId = params.value("thingId").toUuid(); ThingId thingId = params.value("thingId").toUuid();
if (!NymeaCore::instance()->userManager()->accessToThingGranted(thingId, context.token()))
return createReply(statusToReply(Thing::ThingErrorThingNotFound));
IOConnections ioConnections = m_thingManager->ioConnections(thingId); IOConnections ioConnections = m_thingManager->ioConnections(thingId);
QVariantMap returns; QVariantMap returns;
QVariant bla = pack(ioConnections); returns.insert("ioConnections", pack(ioConnections));
returns.insert("ioConnections", pack(ioConnections)); returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorNoError));
return createReply(returns); return createReply(returns);
} }
@ -1153,28 +1228,28 @@ void IntegrationsHandler::thingStateChanged(Thing *thing, const QUuid &stateType
params.insert("minValue", minValue); params.insert("minValue", minValue);
params.insert("maxValue", maxValue); params.insert("maxValue", maxValue);
params.insert("possibleValues", possibleValues); params.insert("possibleValues", possibleValues);
emit StateChanged(params); emit StateChanged(params, thing->id());
} }
void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId) void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId)
{ {
QVariantMap params; QVariantMap params;
params.insert("thingId", thingId); params.insert("thingId", thingId);
emit ThingRemoved(params); emit ThingRemoved(params, thingId);
} }
void IntegrationsHandler::thingAddedNotification(Thing *thing) void IntegrationsHandler::thingAddedNotification(Thing *thing)
{ {
QVariantMap params; QVariantMap params;
params.insert("thing", pack(thing)); params.insert("thing", pack(thing));
emit ThingAdded(params); emit ThingAdded(params, thing->id());
} }
void IntegrationsHandler::thingChangedNotification(Thing *thing) void IntegrationsHandler::thingChangedNotification(Thing *thing)
{ {
QVariantMap params; QVariantMap params;
params.insert("thing", pack(thing)); params.insert("thing", pack(thing));
emit ThingChanged(params); emit ThingChanged(params, thing->id());
} }
void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId &paramTypeId, const QVariant &value) void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId &paramTypeId, const QVariant &value)
@ -1183,7 +1258,7 @@ void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId
params.insert("thingId", thingId); params.insert("thingId", thingId);
params.insert("paramTypeId", paramTypeId.toString()); params.insert("paramTypeId", paramTypeId.toString());
params.insert("value", value); params.insert("value", value);
emit ThingSettingChanged(params); emit ThingSettingChanged(params, thingId);
} }
QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const

View File

@ -26,6 +26,7 @@
#define INTEGRATIONSHANDLER_H #define INTEGRATIONSHANDLER_H
#include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonhandler.h"
#include "usermanager/userinfo.h"
#include "integrations/thingmanager.h" #include "integrations/thingmanager.h"
namespace nymeaserver { namespace nymeaserver {
@ -61,8 +62,9 @@ public:
Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap &params, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap &params, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap &params, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetStateValue(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetStateValues(const QVariantMap &params) const; Q_INVOKABLE JsonReply *GetStateValue(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetStateValues(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *BrowseThing(const QVariantMap &params, const JsonContext &context) const; Q_INVOKABLE JsonReply *BrowseThing(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap &params, const JsonContext &context) const; Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap &params, const JsonContext &context) const;
@ -71,7 +73,7 @@ public:
Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap &params); Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ConnectIO(const QVariantMap &params); Q_INVOKABLE JsonReply *ConnectIO(const QVariantMap &params);
Q_INVOKABLE JsonReply *DisconnectIO(const QVariantMap &params); Q_INVOKABLE JsonReply *DisconnectIO(const QVariantMap &params);
@ -79,15 +81,20 @@ public:
signals: signals:
void PluginConfigurationChanged(const QVariantMap &params); void PluginConfigurationChanged(const QVariantMap &params);
void StateChanged(const QVariantMap &params); // Thing permission relevant notifications
void ThingRemoved(const QVariantMap &params); void StateChanged(const QVariantMap &params, const ThingId &thingId);
void ThingAdded(const QVariantMap &params); void ThingRemoved(const QVariantMap &params, const ThingId &thingId);
void ThingChanged(const QVariantMap &params); void ThingAdded(const QVariantMap &params, const ThingId &thingId);
void ThingSettingChanged(const QVariantMap &params); void ThingChanged(const QVariantMap &params, const ThingId &thingId);
void EventTriggered(const QVariantMap &params); void ThingSettingChanged(const QVariantMap &params, const ThingId &thingId);
void EventTriggered(const QVariantMap &params, const ThingId &thingId);
void IOConnectionAdded(const QVariantMap &params); void IOConnectionAdded(const QVariantMap &params);
void IOConnectionRemoved(const QVariantMap &params); void IOConnectionRemoved(const QVariantMap &params);
// User specific notifications depending on the thing based permissions
void ThingRemoved(const QVariantMap &params, const nymeaserver::UserInfo &userInfo);
void ThingAdded(const QVariantMap &params, const nymeaserver::UserInfo &userInfo);
private slots: private slots:
void pluginConfigChanged(const PluginId &id, const ParamList &config); void pluginConfigChanged(const PluginId &id, const ParamList &config);

View File

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

View File

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

View File

@ -42,12 +42,13 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
QString description; QString description;
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user."; description = "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If the user has not the permission \"PermissionScopeAccessAllThings\", the list of things this user has access to can be defined in the \"allowedThingIds\" property. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user.";
params.insert("username", enumValueName(String)); params.insert("username", enumValueName(String));
params.insert("password", enumValueName(String)); params.insert("password", enumValueName(String));
params.insert("o:email", enumValueName(String)); params.insert("o:email", enumValueName(String));
params.insert("o:displayName", enumValueName(String)); params.insert("o:displayName", enumValueName(String));
params.insert("o:scopes", flagRef<Types::PermissionScopes>()); params.insert("o:scopes", flagRef<Types::PermissionScopes>());
params.insert("o:allowedThingIds", QVariantList() << enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("CreateUser", description, params, returns); registerMethod("CreateUser", description, params, returns);
@ -55,7 +56,14 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
description = "Change the password for the currently logged in user."; description = "Change the password for the currently logged in user.";
params.insert("newPassword", enumValueName(String)); params.insert("newPassword", enumValueName(String));
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("ChangePassword", description, params, returns); registerMethod("ChangePassword", description, params, returns); // TODO: PermissionScopeChangeUserInfos
params.clear(); returns.clear();
description = "Change the password for the given user. All tokens for this user will be removed in order to force all clients to log in again.";
params.insert("username", enumValueName(String));
params.insert("newPassword", enumValueName(String));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("ChangeUserPassword", description, params, returns);
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Get info about the current token (the currently logged in user)."; description = "Get info about the current token (the currently logged in user).";
@ -67,13 +75,21 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
description = "Get all the tokens for the current user."; description = "Get all the tokens for the current user.";
returns.insert("o:tokenInfoList", objectRef<TokenInfoList>()); returns.insert("o:tokenInfoList", objectRef<TokenInfoList>());
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("GetTokens", description, params, returns); registerMethod("GetTokens", description, params, returns); // TODO: PermissionScopeChangeUserInfos
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Revoke access for a given token."; description = "Get all the tokens for the given username.";
params.insert("username", enumValueName(String));
returns.insert("o:tokenInfoList", objectRef<TokenInfoList>());
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("GetUserTokens", description, params, returns);
params.clear(); returns.clear();
description = "Revoke access for a given token. Depending on the logged in user only the own tokens can be removed. If you are logged in as admin, any token can be removed.";
params.insert("tokenId", enumValueName(Uuid)); params.insert("tokenId", enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("RemoveToken", description, params, returns); registerMethod("RemoveToken", description, params, returns); // TODO: PermissionScopeChangeUserInfos
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Return a list of all users in the system."; description = "Return a list of all users in the system.";
@ -87,9 +103,10 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
registerMethod("RemoveUser", description, params, returns); registerMethod("RemoveUser", description, params, returns);
params.clear(); returns.clear(); params.clear(); returns.clear();
description = "Set the permissions (scopes) for a given user."; description = "Set the permissions (scopes) for a given user. If the user has not the permission \"PermissionScopeAccessAllThings\" the list of thing IDs this user has access to can be defined in the \"allowedThingIds\" property.";
params.insert("username", enumValueName(String)); params.insert("username", enumValueName(String));
params.insert("scopes", flagRef<Types::PermissionScopes>()); params.insert("scopes", flagRef<Types::PermissionScopes>());
params.insert("o:allowedThingIds", QVariantList() << enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>()); returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("SetUserScopes", description, params, returns); registerMethod("SetUserScopes", description, params, returns);
@ -141,7 +158,6 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
params.insert("username", username); params.insert("username", username);
emit UserRemoved(params); emit UserRemoved(params);
}); });
} }
QString UsersHandler::name() const QString UsersHandler::name() const
@ -157,8 +173,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
QString displayName = params.value("displayName").toString(); QString displayName = params.value("displayName").toString();
QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList(); QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList();
Types::PermissionScopes scopes = Types::scopesFromStringList(scopesList); Types::PermissionScopes scopes = Types::scopesFromStringList(scopesList);
QList<ThingId> allowedThingIds;
foreach (const QString &thingIdString, params.value("allowedThingIds").toStringList())
allowedThingIds.append(ThingId(thingIdString));
UserManager::UserError status = m_userManager->createUser(username, password, email, displayName, scopes); UserManager::UserError status = m_userManager->createUser(username, password, email, displayName, scopes, allowedThingIds);
QVariantMap returns; QVariantMap returns;
returns.insert("error", enumValueName<UserManager::UserError>(status)); returns.insert("error", enumValueName<UserManager::UserError>(status));
@ -167,19 +186,20 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
JsonReply *UsersHandler::ChangePassword(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::ChangePassword(const QVariantMap &params, const JsonContext &context)
{ {
QVariantMap ret; QVariantMap returns;
QByteArray currentToken = context.token(); QByteArray currentToken = context.token();
if (currentToken.isEmpty()) { if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection"; qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
if (!m_userManager->verifyToken(currentToken)) { if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
QString newPassword = params.value("newPassword").toString(); QString newPassword = params.value("newPassword").toString();
@ -187,52 +207,82 @@ JsonReply *UsersHandler::ChangePassword(const QVariantMap &params, const JsonCon
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
UserManager::UserError status = m_userManager->changePassword(tokenInfo.username(), newPassword); UserManager::UserError status = m_userManager->changePassword(tokenInfo.username(), newPassword);
ret.insert("error", enumValueName<UserManager::UserError>(status)); returns.insert("error", enumValueName<UserManager::UserError>(status));
return createReply(ret); return createReply(returns);
}
JsonReply *UsersHandler::ChangeUserPassword(const QVariantMap &params, const JsonContext &context)
{
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot change a user password from an unauthenticated connection";
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Cannot change a user password from an unauthenticated connection";
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
QString username = params.value("username").toString();;
QString newPassword = params.value("newPassword").toString();
UserManager::UserError status = m_userManager->changePassword(username, newPassword);
returns.insert("error", enumValueName<UserManager::UserError>(status));
return createReply(returns);
} }
JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params, const JsonContext &context)
{ {
Q_UNUSED(params) Q_UNUSED(params)
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token(); QByteArray currentToken = context.token();
if (currentToken.isEmpty()) { if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection"; qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
if (!m_userManager->verifyToken(currentToken)) { if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
UserInfo userInfo = m_userManager->userInfo(tokenInfo.username()); UserInfo userInfo = m_userManager->userInfo(tokenInfo.username());
ret.insert("userInfo", pack(userInfo)); returns.insert("userInfo", pack(userInfo));
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret); return createReply(returns);
} }
JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext &context)
{ {
Q_UNUSED(params) Q_UNUSED(params)
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token(); QByteArray currentToken = context.token();
if (currentToken.isEmpty()) { if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
if (!m_userManager->verifyToken(currentToken)) { if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
@ -242,52 +292,87 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext
foreach (const TokenInfo &tokenInfo, tokens) { foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo); retList << pack(tokenInfo);
} }
ret.insert("tokenInfoList", retList); returns.insert("tokenInfoList", retList);
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret); return createReply(returns);
}
JsonReply *UsersHandler::GetUserTokens(const QVariantMap &params, const JsonContext &context)
{
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection";
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
QString username = params.value("username").toString();;
qCDebug(dcJsonRpc()) << "Fetching tokens for user" << username;
QList<TokenInfo> tokens = m_userManager->tokens(username);
QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo);
}
returns.insert("tokenInfoList", retList);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(returns);
} }
JsonReply *UsersHandler::RemoveToken(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::RemoveToken(const QVariantMap &params, const JsonContext &context)
{ {
QVariantMap ret; QVariantMap returns;
QByteArray currentToken = context.token(); QByteArray currentToken = context.token();
if (currentToken.isEmpty()) { if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection."; qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection.";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
if (!m_userManager->verifyToken(currentToken)) { if (!m_userManager->verifyToken(currentToken)) {
// Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token // Might happen if the client is connecting via an unauthenticated connection but tries to sneak in an invalid token
qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?"; qCWarning(dcJsonRpc()) << "Invalid token. Is this an unauthenticated connection?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
QUuid tokenId = params.value("tokenId").toUuid(); QUuid tokenId = params.value("tokenId").toUuid();
TokenInfo tokenToRemove = m_userManager->tokenInfo(tokenId); TokenInfo tokenToRemove = m_userManager->tokenInfo(tokenId);
if (tokenToRemove.id().isNull()) { if (tokenToRemove.id().isNull()) {
qCWarning(dcJsonRpc()) << "Token with ID" << tokenId << "not found"; qCWarning(dcJsonRpc()) << "Token with ID" << tokenId << "not found";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorTokenNotFound)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorTokenNotFound));
return createReply(ret); return createReply(returns);
} }
TokenInfo currentTokenInfo = m_userManager->tokenInfo(currentToken); TokenInfo currentTokenInfo = m_userManager->tokenInfo(currentToken);
if (currentTokenInfo.username() != tokenToRemove.username()) { if (currentTokenInfo.username() != tokenToRemove.username()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from another user!"; qCWarning(dcJsonRpc()) << "Cannot remove a token from another user!";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
qCDebug(dcJsonRpc()) << "Removing token" << tokenId << "for user" << currentTokenInfo.username(); qCDebug(dcJsonRpc()) << "Removing token" << tokenId << "for user" << currentTokenInfo.username();
UserManager::UserError error = m_userManager->removeToken(tokenId); UserManager::UserError error = m_userManager->removeToken(tokenId);
ret.insert("error", enumValueName<UserManager::UserError>(error)); returns.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(ret); return createReply(returns);
} }
JsonReply *UsersHandler::GetUsers(const QVariantMap &params) JsonReply *UsersHandler::GetUsers(const QVariantMap &params)
{ {
Q_UNUSED(params) Q_UNUSED(params)
QVariantMap reply; QVariantMap reply;
reply.insert("users", pack(m_userManager->users())); reply.insert("users", pack(m_userManager->users()));
return createReply(reply); return createReply(reply);
@ -306,9 +391,20 @@ JsonReply *UsersHandler::RemoveUser(const QVariantMap &params, const JsonContext
JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonContext &context)
{ {
Q_UNUSED(context) Q_UNUSED(context)
qCWarning(dcJsonRpc()) << params;
QString username = params.value("username").toString(); QString username = params.value("username").toString();
Types::PermissionScopes scopes = Types::scopesFromStringList(params.value("scopes").toStringList()); Types::PermissionScopes scopes = Types::scopesFromStringList(params.value("scopes").toStringList());
UserManager::UserError error = m_userManager->setUserScopes(username, scopes); QList<ThingId> allowedThingIds;
if (params.contains("allowedThingIds")) {
allowedThingIds = Types::thingIdsFromStringList(params.value("allowedThingIds").toStringList());
} else {
allowedThingIds = m_userManager->userInfo(username).allowedThingIds();
}
UserManager::UserError error = m_userManager->setUserScopes(username, scopes, allowedThingIds);
QVariantMap returns; QVariantMap returns;
returns.insert("error", enumValueName<UserManager::UserError>(error)); returns.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(returns); return createReply(returns);
@ -316,7 +412,7 @@ JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonCont
JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContext &context) JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContext &context)
{ {
QVariantMap ret; QVariantMap returns;
TokenInfo callingTokenInfo = m_userManager->tokenInfo(context.token()); TokenInfo callingTokenInfo = m_userManager->tokenInfo(context.token());
QString username; QString username;
@ -328,8 +424,8 @@ JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContex
} }
if (callingTokenInfo.username() != username && !m_userManager->userInfo(callingTokenInfo.username()).scopes().testFlag(Types::PermissionScopeAdmin)) { if (callingTokenInfo.username() != username && !m_userManager->userInfo(callingTokenInfo.username()).scopes().testFlag(Types::PermissionScopeAdmin)) {
ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied));
return createReply(ret); return createReply(returns);
} }
UserInfo changedUserInfo = m_userManager->userInfo(username); UserInfo changedUserInfo = m_userManager->userInfo(username);
@ -347,8 +443,8 @@ JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContex
displayName = changedUserInfo.displayName(); displayName = changedUserInfo.displayName();
} }
UserManager::UserError status = m_userManager->setUserInfo(username, email, displayName); UserManager::UserError status = m_userManager->setUserInfo(username, email, displayName);
ret.insert("error", enumValueName(status)); returns.insert("error", enumValueName(status));
return createReply(ret); return createReply(returns);
} }
} }

View File

@ -43,8 +43,10 @@ public:
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params); Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ChangeUserPassword(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetTokens(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *GetTokens(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetUserTokens(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetUsers(const QVariantMap &params); Q_INVOKABLE JsonReply *GetUsers(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap &params, const JsonContext &context); Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap &params, const JsonContext &context);

View File

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

View File

@ -79,6 +79,16 @@ void UserInfo::setScopes(Types::PermissionScopes scopes)
m_scopes = scopes; m_scopes = scopes;
} }
void UserInfo::setAllowedThingIds(const QList<ThingId> &allowedThingIds)
{
m_allowedThingIds = allowedThingIds;
}
QList<ThingId> UserInfo::allowedThingIds() const
{
return m_allowedThingIds;
}
QVariant UserInfoList::get(int index) const QVariant UserInfoList::get(int index) const
{ {
return QVariant::fromValue(at(index)); return QVariant::fromValue(at(index));

View File

@ -39,6 +39,7 @@ class UserInfo
Q_PROPERTY(QString email READ email) Q_PROPERTY(QString email READ email)
Q_PROPERTY(QString displayName READ displayName) Q_PROPERTY(QString displayName READ displayName)
Q_PROPERTY(Types::PermissionScopes scopes READ scopes) Q_PROPERTY(Types::PermissionScopes scopes READ scopes)
Q_PROPERTY(QList<ThingId> allowedThingIds READ allowedThingIds)
public: public:
UserInfo(); UserInfo();
@ -56,13 +57,18 @@ public:
Types::PermissionScopes scopes() const; Types::PermissionScopes scopes() const;
void setScopes(Types::PermissionScopes scopes); void setScopes(Types::PermissionScopes scopes);
void setAllowedThingIds(const QList<ThingId> &allowedThingIds);
QList<ThingId> allowedThingIds() const;
private: private:
QString m_username; QString m_username;
QString m_email; QString m_email;
QString m_displayName; QString m_displayName;
Types::PermissionScopes m_scopes = Types::PermissionScopeNone; Types::PermissionScopes m_scopes = Types::PermissionScopeNone;
QList<ThingId> m_allowedThingIds;
}; };
class UserInfoList: public QList<UserInfo> class UserInfoList: public QList<UserInfo>
{ {
Q_GADGET Q_GADGET
@ -71,5 +77,10 @@ public:
Q_INVOKABLE QVariant get(int index) const; Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant); Q_INVOKABLE void put(const QVariant &variant);
}; };
} }
Q_DECLARE_METATYPE(nymeaserver::UserInfo);
Q_DECLARE_METATYPE(nymeaserver::UserInfoList);
#endif // USERINFO_H #endif // USERINFO_H

View File

@ -63,7 +63,6 @@
*/ */
#include "usermanager.h" #include "usermanager.h"
#include "nymeasettings.h"
#include "loggingcategories.h" #include "loggingcategories.h"
#include "pushbuttondbusservice.h" #include "pushbuttondbusservice.h"
#include "nymeacore.h" #include "nymeacore.h"
@ -96,7 +95,7 @@ UserManager::UserManager(const QString &dbName, QObject *parent):
if (!initDB()) { if (!initDB()) {
qCWarning(dcUserManager()) << "Error initializing user database. Trying to correct it."; qCWarning(dcUserManager()) << "Error initializing user database. Trying to correct it.";
if (QFileInfo(m_db.databaseName()).exists()) { if (QFileInfo::exists(m_db.databaseName())) {
rotate(m_db.databaseName()); rotate(m_db.databaseName());
if (!initDB()) { if (!initDB()) {
qCWarning(dcUserManager()) << "Error fixing user database. Giving up. Users can't be stored."; qCWarning(dcUserManager()) << "Error fixing user database. Giving up. Users can't be stored.";
@ -141,18 +140,20 @@ UserInfoList UserManager::users() const
qCWarning(dcUserManager()) << "Unable to execute SQL query" << userQuery << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager()) << "Unable to execute SQL query" << userQuery << m_db.lastError().databaseText() << m_db.lastError().driverText();
return users; return users;
} }
while (resultQuery.next()) { while (resultQuery.next()) {
UserInfo info = UserInfo(resultQuery.value("username").toString()); UserInfo info = UserInfo(resultQuery.value("username").toString());
info.setEmail(resultQuery.value("email").toString()); info.setEmail(resultQuery.value("email").toString());
info.setDisplayName(resultQuery.value("displayName").toString()); info.setDisplayName(resultQuery.value("displayName").toString());
info.setScopes(Types::scopesFromStringList(resultQuery.value("scopes").toString().split(','))); info.setScopes(Types::scopesFromStringList(resultQuery.value("scopes").toString().split(',')));
info.setAllowedThingIds(Types::thingIdsFromStringList(resultQuery.value("allowedThingIds").toString().split(',')));
users.append(info); users.append(info);
} }
return users; return users;
} }
/*! Creates a new user with the given \a username and \a password. Returns the \l UserError to inform about the result. */ /*! Creates a new user with the given \a username and \a password. Returns the \l UserError to inform about the result. */
UserManager::UserError UserManager::createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes) UserManager::UserError UserManager::createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds)
{ {
if (!validateUsername(username)) { if (!validateUsername(username)) {
qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username; qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username;
@ -164,26 +165,53 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
return UserErrorBadPassword; return UserErrorBadPassword;
} }
if (!validateScopes(scopes)) {
// The method warns about he specific validation
return UserErrorInconsistantScopes;
}
// Verify thing IDs, if there is no thing with this id, we don't save it and it will not be verified.
// We don't return an error, the thing might have dissapeared
QList<ThingId> thingIds;
ThingManager *thingManager = NymeaCore::instance()->thingManager();
if (!thingManager) {
qCWarning(dcUserManager()) << "Cannot validate allowed things for user" << username
<< "because thing manager is not available yet. Skipping validation.";
thingIds = allowedThingIds;
} else {
foreach (const ThingId &thingId, allowedThingIds) {
if (thingManager->configuredThings().findById(thingId) == nullptr) {
qCWarning(dcUserManager()) << "Cannot set user scope for" << username << "because there is no thing with ID ";
} else {
thingIds.append(thingId);
}
}
}
QSqlQuery checkForDuplicateUserQuery(m_db); QSqlQuery checkForDuplicateUserQuery(m_db);
checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = ?;"); checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = :username;");
checkForDuplicateUserQuery.bindValue(":username", username.toLower());
// Note: We're using toLower() on the username mainly for the reason that in old versions the username used to be an email address // Note: We're using toLower() on the username mainly for the reason that in old versions the username used to be an email address
checkForDuplicateUserQuery.addBindValue(username.toLower());
checkForDuplicateUserQuery.exec(); checkForDuplicateUserQuery.exec();
if (checkForDuplicateUserQuery.first()) { if (checkForDuplicateUserQuery.first()) {
qCWarning(dcUserManager) << "Username already in use"; qCWarning(dcUserManager) << "Username" << username << "already in use";
return UserErrorDuplicateUserId; return UserErrorDuplicateUserId;
} }
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); static QRegularExpression bracketsRe("[{}]");
QByteArray salt = QUuid::createUuid().toString().remove(bracketsRe).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64(); QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QSqlQuery query(m_db); QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) VALUES(?, ?, ?, ?, ?, ?);"); query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds)"
query.addBindValue(username.toLower()); "VALUES(:username, :email, :displayName, :password, :salt, :scopes, :allowedThingIds);");
query.addBindValue(email);
query.addBindValue(displayName); query.bindValue(":username", username.toLower());
query.addBindValue(QString::fromUtf8(hashedPassword)); query.bindValue(":email", email);
query.addBindValue(QString::fromUtf8(salt)); query.bindValue(":displayName", displayName);
query.addBindValue(Types::scopesToStringList(scopes).join(',')); query.bindValue(":password", QString::fromUtf8(hashedPassword));
query.bindValue(":salt", QString::fromUtf8(salt));
query.bindValue(":scopes", Types::scopesToStringList(scopes).join(','));
query.bindValue(":allowedThingIds", Types::thingIdsToStringList(thingIds).join(','));
query.exec(); query.exec();
if (query.lastError().type() != QSqlError::NoError) { if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error creating user:" << query.lastError().databaseText() << query.lastError().driverText(); qCWarning(dcUserManager) << "Error creating user:" << query.lastError().databaseText() << query.lastError().driverText();
@ -222,14 +250,15 @@ UserManager::UserError UserManager::changePassword(const QString &username, cons
// Update the password // Update the password
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(newPassword + salt).toUtf8(), QCryptographicHash::Sha512).toBase64(); QByteArray hashedPassword = QCryptographicHash::hash(QString(newPassword + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QString updatePasswordQueryString = QString("UPDATE users SET password = \"%1\", salt = \"%2\" WHERE lower(username) = \"%3\";")
.arg(QString::fromUtf8(hashedPassword))
.arg(QString::fromUtf8(salt))
.arg(username.toLower());
QSqlQuery updatePasswordQuery(m_db); QSqlQuery updatePasswordQuery(m_db);
if (!updatePasswordQuery.exec(updatePasswordQueryString)) { updatePasswordQuery.prepare("UPDATE users SET password = :password, salt = :salt WHERE lower(username) = :username;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << updatePasswordQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); updatePasswordQuery.bindValue(":password", QString::fromUtf8(hashedPassword));
updatePasswordQuery.bindValue(":salt", QString::fromUtf8(salt));
updatePasswordQuery.bindValue(":username", username.toLower());
if (!updatePasswordQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << updatePasswordQuery.executedQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserErrorBackendError; return UserErrorBackendError;
} }
@ -244,7 +273,7 @@ UserManager::UserError UserManager::changePassword(const QString &username, cons
UserManager::UserError UserManager::removeUser(const QString &username) UserManager::UserError UserManager::removeUser(const QString &username)
{ {
QString dropUserQueryString = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower()); QString dropUserQueryString = QString("DELETE FROM users WHERE lower(username) = \"%1\";").arg(username.toLower());
QSqlQuery dropUserQuery(m_db); QSqlQuery dropUserQuery(m_db);
if (!dropUserQuery.exec(dropUserQueryString)) { if (!dropUserQuery.exec(dropUserQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << dropUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager()) << "Unable to execute SQL query" << dropUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
@ -265,30 +294,95 @@ UserManager::UserError UserManager::removeUser(const QString &username)
return UserErrorNoError; return UserErrorNoError;
} }
UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes) UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds)
{ {
if (!validateScopes(scopes)) {
// The method warns about he specific validation
return UserErrorInconsistantScopes;
}
// Verify thing IDs, if there is no thing with this id, we don't save it and it will not be verified.
// We don't return an error, the thing might have dissapeared
QList<ThingId> thingIds;
ThingManager *thingManager = NymeaCore::instance()->thingManager();
if (!thingManager) {
qCWarning(dcUserManager()) << "Cannot validate allowed things for user" << username
<< "because thing manager is not available yet. Skipping validation.";
thingIds = allowedThingIds;
} else {
foreach (const ThingId &thingId, allowedThingIds) {
if (thingManager->configuredThings().findById(thingId) == nullptr) {
qCWarning(dcUserManager()) << "The user" << username << "should have access to thing with ID"
<< thingId.toString() << "but there is no such thing. Ignoring value.";
} else {
thingIds.append(thingId);
}
}
}
QList<ThingId> thingsAppeared;
QList<ThingId> thingsDisappeared;
// Get the current allowed things
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
// Restricted thing access, let's notify this user if any things appeared or dissapeard for the user
UserInfo currentUserInfo = userInfo(username);
// Get new appeared things for this user
foreach (const ThingId &thingId, thingIds) {
if (currentUserInfo.allowedThingIds().contains(thingId))
continue;
qCDebug(dcUserManager()) << "Thing with ID" << thingId.toString() << "now allowed for this user any more. Notify user" << username << "that thing appeared.";
thingsAppeared.append(thingId);
}
// Get disappeared things for this user
foreach (const ThingId &thingId, currentUserInfo.allowedThingIds()) {
if (thingIds.contains(thingId))
continue;
qCDebug(dcUserManager()) << "Thing with ID" << thingId.toString() << "not allowed for this user any more. Notify user" << username << "that thing dissappeared.";
thingsDisappeared.append(thingId);
}
}
QString scopesString = Types::scopesToStringList(scopes).join(','); QString scopesString = Types::scopesToStringList(scopes).join(',');
QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(',');
qCDebug(dcUserManager()) << "Updating scopes of user" << username << "Scopes:" << scopes << "Allowed things:" << allowedThingIds;
QSqlQuery setScopesQuery(m_db); QSqlQuery setScopesQuery(m_db);
setScopesQuery.prepare("UPDATE users SET scopes = ? WHERE username = ?"); setScopesQuery.prepare("UPDATE users SET scopes = :scopes, allowedThingIds = :allowedThingIds WHERE username = :username;");
setScopesQuery.addBindValue(scopesString); setScopesQuery.bindValue(":username", username);
setScopesQuery.addBindValue(username); setScopesQuery.bindValue(":scopes", scopesString);
setScopesQuery.exec(); setScopesQuery.bindValue(":allowedThingIds", allowedThingIdsString);
if (setScopesQuery.lastError().type() != QSqlError::NoError) { if (!setScopesQuery.exec()) {
qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText(); qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText();
return UserErrorBackendError; return UserErrorBackendError;
} }
emit userChanged(username); emit userChanged(username);
// Notify after updating the user information
UserInfo ui = userInfo(username);
foreach (const ThingId &thingId, thingsAppeared)
emit userThingRestrictionsChanged(ui, thingId, true);
foreach (const ThingId &thingId, thingsDisappeared)
emit userThingRestrictionsChanged(ui, thingId, false);
return UserErrorNoError; return UserErrorNoError;
} }
UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName) UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName)
{ {
QSqlQuery query(m_db); QSqlQuery query(m_db);
query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;"); query.prepare("UPDATE users SET email = :email, displayName = :displayName WHERE username = :username;");
query.addBindValue(email); query.bindValue(":email", email);
query.addBindValue(displayName); query.bindValue(":displayName", displayName);
query.addBindValue(username); query.bindValue(":username", username);
query.exec(); query.exec();
if (query.lastError().type() != QSqlError::NoError) { if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery(); qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
@ -315,13 +409,14 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
} }
QSqlQuery passwordQuery(m_db); QSqlQuery passwordQuery(m_db);
passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = ?;"); passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = :username;");
passwordQuery.addBindValue(username.toLower()); passwordQuery.bindValue(":username", username.toLower());
passwordQuery.exec(); passwordQuery.exec();
if (!passwordQuery.first()) { if (!passwordQuery.first()) {
qCWarning(dcUserManager) << "No such username" << username; qCWarning(dcUserManager) << "No such username" << username;
return QByteArray(); return QByteArray();
} }
QByteArray salt = passwordQuery.value("salt").toByteArray(); QByteArray salt = passwordQuery.value("salt").toByteArray();
QByteArray hashedPassword = passwordQuery.value("password").toByteArray(); QByteArray hashedPassword = passwordQuery.value("password").toByteArray();
@ -331,16 +426,18 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
} }
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
QString storeTokenQueryString = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
.arg(QUuid::createUuid().toString())
.arg(username.toLower())
.arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(deviceName);
QSqlQuery storeTokenQuery(m_db); QSqlQuery storeTokenQuery(m_db);
if (!storeTokenQuery.exec(storeTokenQueryString)) { storeTokenQuery.prepare("INSERT INTO tokens (id, username, token, creationdate, devicename)"
qCWarning(dcUserManager()) << "Unable to execute SQL query" << storeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); "VALUES (:id, :username, :token, :creationdate, :devicename)");
storeTokenQuery.bindValue(":id", QUuid::createUuid().toString());
storeTokenQuery.bindValue(":username", username.toLower());
storeTokenQuery.bindValue(":token", QString::fromUtf8(token));
storeTokenQuery.bindValue(":creationdate", NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
storeTokenQuery.bindValue(":devicename", deviceName);
if (!storeTokenQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << storeTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return QByteArray(); return QByteArray();
} }
@ -391,18 +488,16 @@ void UserManager::cancelPushButtonAuth(int transactionId)
*/ */
UserInfo UserManager::userInfo(const QString &username) const UserInfo UserManager::userInfo(const QString &username) const
{ {
QString getUserQueryString = QString("SELECT * FROM users WHERE lower(username) = \"%1\";")
.arg(username);
QSqlQuery getUserQuery(m_db); QSqlQuery getUserQuery(m_db);
if (!getUserQuery.exec(getUserQueryString)) { getUserQuery.prepare("SELECT * FROM users WHERE lower(username) = :username;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); getUserQuery.bindValue(":username", username);
if (!getUserQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getUserQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserInfo(); return UserInfo();
} }
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for user" << username << "failed:" << getUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager) << "Query for user" << username << "failed:" << getUserQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserInfo(); return UserInfo();
} }
@ -413,7 +508,7 @@ UserInfo UserManager::userInfo(const QString &username) const
userInfo.setEmail(getUserQuery.value("email").toString()); userInfo.setEmail(getUserQuery.value("email").toString());
userInfo.setDisplayName(getUserQuery.value("displayName").toString()); userInfo.setDisplayName(getUserQuery.value("displayName").toString());
userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(','))); userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(',')));
userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(',')));
return userInfo; return userInfo;
} }
@ -422,8 +517,8 @@ QList<TokenInfo> UserManager::tokens(const QString &username) const
QList<TokenInfo> ret; QList<TokenInfo> ret;
QSqlQuery query(m_db); QSqlQuery query(m_db);
query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = ?;"); query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = :username;");
query.addBindValue(username.toLower()); query.bindValue(":username", username.toLower());
query.exec(); query.exec();
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for tokens failed:" << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery(); qCWarning(dcUserManager) << "Query for tokens failed:" << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
@ -443,17 +538,16 @@ TokenInfo UserManager::tokenInfo(const QByteArray &token) const
return TokenInfo(); return TokenInfo();
} }
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
QSqlQuery getTokenQuery(m_db); QSqlQuery getTokenQuery(m_db);
if (!getTokenQuery.exec(getTokenQueryString)) { getTokenQuery.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = :token;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); getTokenQuery.bindValue(":token", QString::fromUtf8(token));
if (!getTokenQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return TokenInfo(); return TokenInfo();
} }
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for token failed:" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager) << "Query for token failed:" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return TokenInfo(); return TokenInfo();
} }
@ -465,18 +559,16 @@ TokenInfo UserManager::tokenInfo(const QByteArray &token) const
TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const
{ {
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
QSqlQuery getTokenQuery(m_db); QSqlQuery getTokenQuery(m_db);
if (!getTokenQuery.exec(getTokenQueryString)) { getTokenQuery.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = :id;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); getTokenQuery.bindValue(":id", tokenId.toString());
if (!getTokenQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return TokenInfo(); return TokenInfo();
} }
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for token failed:" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager) << "Query for token failed:" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return TokenInfo(); return TokenInfo();
} }
@ -489,21 +581,22 @@ TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const
/*! Removes the token with the given \a tokenId. Returns \l{UserError} to inform about the result. */ /*! Removes the token with the given \a tokenId. Returns \l{UserError} to inform about the result. */
UserManager::UserError UserManager::removeToken(const QUuid &tokenId) UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
{ {
QString removeTokenQueryString = QString("DELETE FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
QSqlQuery removeTokenQuery(m_db); QSqlQuery removeTokenQuery(m_db);
if (!removeTokenQuery.exec(removeTokenQueryString)) { removeTokenQuery.prepare("DELETE FROM tokens WHERE id = :id;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << removeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); removeTokenQuery.bindValue(":id", tokenId.toString());
if (!removeTokenQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << removeTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserErrorBackendError; return UserErrorBackendError;
} }
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Removing token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << removeTokenQueryString; qCWarning(dcUserManager) << "Removing token failed:" << removeTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return UserErrorBackendError; return UserErrorBackendError;
} }
if (removeTokenQuery.numRowsAffected() != 1) { if (removeTokenQuery.numRowsAffected() != 1) {
qCWarning(dcUserManager) << "Token not found in DB"; qCWarning(dcUserManager) << "Tried to remove token, but the token could not be found in the DB.";
return UserErrorTokenNotFound; return UserErrorTokenNotFound;
} }
@ -518,27 +611,65 @@ bool UserManager::verifyToken(const QByteArray &token)
qCWarning(dcUserManager) << "Token failed character validation" << token; qCWarning(dcUserManager) << "Token failed character validation" << token;
return false; return false;
} }
QString getTokenQueryString = QString("SELECT * FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
QSqlQuery getTokenQuery(m_db); QSqlQuery getTokenQuery(m_db);
if (!getTokenQuery.exec(getTokenQueryString)) { getTokenQuery.prepare("SELECT * FROM tokens WHERE token = :token;");
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText(); getTokenQuery.bindValue(":token", QString::fromUtf8(token));
if (!getTokenQuery.exec()) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
return false; return false;
} }
if (m_db.lastError().type() != QSqlError::NoError) { if (m_db.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQueryString; qCWarning(dcUserManager) << "Query for token failed:" << getTokenQuery.lastQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQuery.lastQuery();
return false; return false;
} }
if (!getTokenQuery.first()) { if (!getTokenQuery.first()) {
qCDebug(dcUserManager) << "Authorization failed for token" << token; qCDebug(dcUserManager) << "Authorization failed for token" << token;
return false; return false;
} }
//qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
return true; return true;
} }
bool UserManager::hasRestrictedThingAccess(const QByteArray &token) const
{
UserInfo ui = userInfo(tokenInfo(token).username());
return !ui.scopes().testFlag(Types::PermissionScopeAccessAllThings);
}
bool UserManager::accessToThingGranted(const ThingId &thingId, const QByteArray &token)
{
if (!hasRestrictedThingAccess(token))
return true;
return getAllowedThingIdsForToken(token).contains(thingId);
}
QList<ThingId> UserManager::getAllowedThingIdsForToken(const QByteArray &token) const
{
return userInfo(tokenInfo(token).username()).allowedThingIds();
}
void UserManager::onThingRemoved(const ThingId &thingId)
{
// If a thing has been removed from the system, clean up any thing based permissions
foreach (const UserInfo &userInfo, users()) {
if (userInfo.allowedThingIds().contains(thingId)) {
QList<ThingId> allowedThingIds = userInfo.allowedThingIds();
allowedThingIds.removeAll(thingId);
if (setUserScopes(userInfo.username(), userInfo.scopes(), allowedThingIds) != UserErrorNoError) {
qCWarning(dcUserManager()) << "Failed to remove thing with ID" << thingId.toString() << "from allowed things of user" << userInfo.username();
} else {
qCDebug(dcUserManager()) << "Removed thing with ID" << thingId.toString() << "from allowed things of user" << userInfo.username();
}
}
}
}
bool UserManager::initDB() bool UserManager::initDB()
{ {
m_db.close(); m_db.close();
@ -549,26 +680,32 @@ bool UserManager::initDB()
} }
int currentVersion = -1; int currentVersion = -1;
int newVersion = 1; int newVersion = 2;
if (m_db.tables().contains("metadata")) { if (m_db.tables().contains("metadata")) {
QSqlQuery query(m_db); QSqlQuery query(m_db);
if (!query.exec("SELECT data FROM metadata WHERE `key` = 'version';")) { if (!query.exec("SELECT data FROM metadata WHERE key = 'version';")) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << query.executedQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText(); qCWarning(dcUserManager()) << "Unable to execute SQL query" << query.executedQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
} else if (query.next()) { } else if (query.next()) {
currentVersion = query.value("data").toInt(); currentVersion = query.value("data").toInt();
qCInfo(dcUserManager()) << "Current database version is" << currentVersion;
if (currentVersion == newVersion) {
qCInfo(dcUserManager()) << "The database version is up to date";
}
} }
} }
if (!m_db.tables().contains("users")) { if (!m_db.tables().contains("users")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata..."; qCDebug(dcUserManager()) << "No \"users\" table found. Creating the table...";
QSqlQuery query(m_db); QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE PRIMARY KEY, email VARCHAR(40), displayName VARCHAR(40), password VARCHAR(100), salt VARCHAR(100), scopes TEXT);") || m_db.lastError().isValid()) { if (!query.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE PRIMARY KEY, email VARCHAR(40), displayName VARCHAR(40), password VARCHAR(100), salt VARCHAR(100), scopes TEXT, allowedThingIds TEXT);") || m_db.lastError().isValid()) {
dumpDBError("Error initializing user database (table users)."); dumpDBError("Error initializing user database (table users).");
m_db.close(); m_db.close();
return false; return false;
} }
} else { } else {
if (currentVersion < 1) { if (currentVersion < 1) {
qCDebug(dcUserManager()) << "Start user table database migration to version 1";
QSqlQuery query = QSqlQuery(m_db); QSqlQuery query = QSqlQuery(m_db);
if (!query.exec("ALTER TABLE users ADD COLUMN scopes TEXT;") || m_db.lastError().isValid()) { if (!query.exec("ALTER TABLE users ADD COLUMN scopes TEXT;") || m_db.lastError().isValid()) {
dumpDBError("Error migrating user database (table users)."); dumpDBError("Error migrating user database (table users).");
@ -608,86 +745,118 @@ bool UserManager::initDB()
m_db.close(); m_db.close();
return false; return false;
} }
currentVersion = 1;
qCDebug(dcUserManager()) << "Migrated successfully users table to database version 1";
}
if (currentVersion < 2) {
// - Add new "allowedThingIds" row into the users table
// - New permission has been added "PermissionScopeAccessAllThings", the existing users require
// all this permission in order to have an unchainged behavior
qCDebug(dcUserManager()) << "Migrating user table to version 2";
// - Add new "allowedThingIds" row into the users table, it remains is empty at this point
QSqlQuery query = QSqlQuery(m_db);
if (!query.exec("ALTER TABLE users ADD COLUMN allowedThingIds TEXT;") || m_db.lastError().isValid()) {
dumpDBError("Error migrating user database (table users).");
m_db.close();
return false;
}
if (!m_db.transaction()) {
dumpDBError("Error starting transaction for migrating user database (table users).");
return false;
}
QSqlQuery selectQuery(m_db);
if (!selectQuery.exec("SELECT username, scopes FROM users")) {
dumpDBError("Select failed: " + selectQuery.lastError().text());
return false;
}
QSqlQuery updateQuery(m_db);
updateQuery.prepare("UPDATE users SET scopes = :scopes WHERE username = :username");
while (selectQuery.next()) {
QString username = selectQuery.value("username").toString();
Types::PermissionScopes scopes = Types::scopesFromStringList(selectQuery.value("scopes").toString().split(','));
// In case this is an admin, make sure we store only the Admin scope
if (!scopes.testFlag(Types::PermissionScopeAdmin)) {
scopes.setFlag(Types::PermissionScopeAccessAllThings);
}
updateQuery.bindValue(":scopes", Types::scopesToStringList(scopes).join(','));
updateQuery.bindValue(":username", username);
if (!updateQuery.exec()) {
qCWarning(dcUserManager()) << "Update failed for username" << username << ":" << updateQuery.lastError().text();
m_db.rollback();
return false;
}
}
if (!m_db.commit()) {
dumpDBError("Error migrating user database (table users) to version 2. Rollback.");
m_db.rollback();
return false;
}
qCDebug(dcUserManager()) << "Migrated successfully users table to database version 2";
} }
} }
if (!m_db.tables().contains("tokens")) { if (!m_db.tables().contains("tokens")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata..."; qCDebug(dcUserManager()) << "No \"tokens\" table found. Creating the table...";
QSqlQuery query(m_db); QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));") || m_db.lastError().isValid()) { if (!query.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));") || m_db.lastError().isValid()) {
dumpDBError("Error initializing user database (table tokens)."); dumpDBError("Error initializing user database (table tokens)");
m_db.close(); m_db.close();
return false; return false;
} }
} }
if (m_db.tables().contains("metadata")) { if (!m_db.tables().contains("metadata")) {
if (currentVersion < newVersion) { qCDebug(dcUserManager()) << "No \"metadata\" table found. Creating the table...";
QSqlQuery query(m_db);
if (!query.exec(QString("UPDATE metadata SET data = %1 WHERE `key` = 'version')").arg(newVersion)) || m_db.lastError().isValid()) {
dumpDBError("Error updating up user database schema version!");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Successfully migrated user database.";
}
} else {
QSqlQuery query(m_db); QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE metadata (`key` VARCHAR(10), data VARCHAR(40));") || m_db.lastError().isValid()) { if (!query.exec("CREATE TABLE metadata (key VARCHAR(10), data VARCHAR(40));") || m_db.lastError().isValid()) {
dumpDBError("Error setting up user database (table metadata)!"); dumpDBError("Error setting up user database (table metadata)!");
m_db.close(); m_db.close();
return false; return false;
} }
query = QSqlQuery(m_db); query = QSqlQuery(m_db);
if (!query.exec(QString("INSERT INTO metadata (`key`, `data`) VALUES ('version', %1);").arg(newVersion)) || m_db.lastError().isValid()) { query.prepare("INSERT INTO metadata (key, data) VALUES ('version', :version);");
query.bindValue(":version", newVersion);
if (!query.exec() || m_db.lastError().isValid()) {
dumpDBError("Error setting up user database (setting version metadata)!"); dumpDBError("Error setting up user database (setting version metadata)!");
m_db.close(); m_db.close();
return false; return false;
} }
qCInfo(dcUserManager()) << "Successfully initialized user database."; } else {
} // All migrations have been done
if (currentVersion < newVersion) {
// 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); QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) values(?, ?, ?, ?, ?, ?);"); query.prepare("UPDATE metadata SET data = :version WHERE key = 'version'");
query.addBindValue(""); query.bindValue(":version", newVersion);
query.addBindValue(""); if (!query.exec() || m_db.lastError().isValid()) {
query.addBindValue("Admin"); dumpDBError("Error updating database version");
query.addBindValue(""); m_db.close();
query.addBindValue(""); return false;
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();
} }
qCInfo(dcUserManager()) << "Finished database migration to version" << newVersion;
} }
} }
qCDebug(dcUserManager()) << "User database initialized successfully";
qCDebug(dcUserManager()) << "User database initialized successfully.";
return true; return true;
} }
void UserManager::rotate(const QString &dbName) void UserManager::rotate(const QString &dbName)
{ {
int index = 1; int index = 1;
while (QFileInfo(QString("%1.%2").arg(dbName).arg(index)).exists()) { while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index)))
index++; index++;
}
qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index); qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index);
QFile f(dbName); QFile f(dbName);
if (!f.rename(QString("%1.%2").arg(dbName).arg(index))) { if (!f.rename(QString("%1.%2").arg(dbName).arg(index))) {
@ -699,38 +868,87 @@ void UserManager::rotate(const QString &dbName)
bool UserManager::validateUsername(const QString &username) const bool UserManager::validateUsername(const QString &username) const
{ {
QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}"); static QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}");
return validator.match(username).hasMatch(); return validator.match(username).hasMatch();
} }
bool UserManager::validatePassword(const QString &password) const bool UserManager::validatePassword(const QString &password) const
{ {
if (password.length() < 8) { if (password.length() < 8)
return false; return false;
}
if (!password.contains(QRegularExpression("[a-z]"))) { static QRegularExpression lowerRe("[a-z]");
if (!password.contains(lowerRe))
return false; return false;
}
if (!password.contains(QRegularExpression("[A-Z]"))) { static QRegularExpression upperRe("[A-Z]");
if (!password.contains(upperRe))
return false; return false;
}
if (!password.contains(QRegularExpression("[0-9]"))) { static QRegularExpression numbersRe("[0-9]");
if (!password.contains(numbersRe))
return false; return false;
}
return true; return true;
} }
bool UserManager::validateToken(const QByteArray &token) const bool UserManager::validateToken(const QByteArray &token) const
{ {
QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)")); static QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)"));
return validator.match(token).hasMatch(); return validator.match(token).hasMatch();
} }
bool UserManager::validateScopes(Types::PermissionScopes scopes) const
{
if (scopes.testFlag(Types::PermissionScopeAdmin) || scopes == Types::PermissionScopeNone || scopes == Types::PermissionScopeControlThings)
return true;
if (scopes.testFlag(Types::PermissionScopeConfigureThings)) {
if (!scopes.testFlag(Types::PermissionScopeControlThings) ||
!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can configure things he must have access to all things and must be able to control them.";
return false;
}
}
// Note: if access to all things, there are no restrictions
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
if (scopes.testFlag(Types::PermissionScopeControlThings) ||
scopes.testFlag(Types::PermissionScopeConfigureRules)||
scopes.testFlag(Types::PermissionScopeExecuteRules)) {
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user has not access to all things, he can not configure them or create/execute rules.";
return false;
}
}
if (scopes.testFlag(Types::PermissionScopeExecuteRules)) {
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can execute rules, he must have access to all things.";
return false;
}
}
if (scopes.testFlag(Types::PermissionScopeConfigureRules)) {
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings) ||
!scopes.testFlag(Types::PermissionScopeExecuteRules)) {
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can create rules, he must have access to all things and be able to execute them.";
return false;
}
}
return true;
}
void UserManager::dumpDBError(const QString &message) void UserManager::dumpDBError(const QString &message)
{ {
qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
} }
void UserManager::evaluateAllowedThingsForUser()
{
}
void UserManager::onPushButtonPressed() void UserManager::onPushButtonPressed()
{ {
if (m_pushButtonTransaction.first == -1) { if (m_pushButtonTransaction.first == -1) {
@ -758,11 +976,11 @@ void UserManager::onPushButtonPressed()
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
QString storeTokenQueryString = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");") QString storeTokenQueryString = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
.arg(QUuid::createUuid().toString()) .arg(QUuid::createUuid().toString())
.arg("") .arg("")
.arg(QString::fromUtf8(token)) .arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) .arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(m_pushButtonTransaction.second); .arg(m_pushButtonTransaction.second);
QSqlQuery storeTokenQuery(m_db); QSqlQuery storeTokenQuery(m_db);
if (!storeTokenQuery.exec(storeTokenQueryString) || m_db.lastError().type() != QSqlError::NoError) { if (!storeTokenQuery.exec(storeTokenQueryString) || m_db.lastError().type() != QSqlError::NoError) {

View File

@ -46,7 +46,8 @@ public:
UserErrorDuplicateUserId, UserErrorDuplicateUserId,
UserErrorBadPassword, UserErrorBadPassword,
UserErrorTokenNotFound, UserErrorTokenNotFound,
UserErrorPermissionDenied UserErrorPermissionDenied,
UserErrorInconsistantScopes
}; };
Q_ENUM(UserError) Q_ENUM(UserError)
@ -55,10 +56,10 @@ public:
bool initRequired() const; bool initRequired() const;
UserInfoList users() const; UserInfoList users() const;
UserError createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes); UserError createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds = QList<ThingId>());
UserError changePassword(const QString &username, const QString &newPassword); UserError changePassword(const QString &username, const QString &newPassword);
UserError removeUser(const QString &username); UserError removeUser(const QString &username);
UserError setUserScopes(const QString &username, Types::PermissionScopes scopes); UserError setUserScopes(const QString &username, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds = QList<ThingId>());
UserError setUserInfo(const QString &username, const QString &email, const QString &displayName); UserError setUserInfo(const QString &username, const QString &email, const QString &displayName);
bool pushButtonAuthAvailable() const; bool pushButtonAuthAvailable() const;
@ -77,21 +78,33 @@ public:
bool verifyToken(const QByteArray &token); bool verifyToken(const QByteArray &token);
bool hasRestrictedThingAccess(const QByteArray &token) const;
bool accessToThingGranted(const ThingId &thingId, const QByteArray &token);
QList<ThingId> getAllowedThingIdsForToken(const QByteArray &token) const;
public slots:
void onThingRemoved(const ThingId &thingId);
signals: signals:
void userAdded(const QString &username); void userAdded(const QString &username);
void userRemoved(const QString &username); void userRemoved(const QString &username);
void userChanged(const QString &username); void userChanged(const QString &username);
void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
void userThingRestrictionsChanged(const nymeaserver::UserInfo &userInfo, const ThingId &thingId, bool accessGranted);
private: private:
bool initDB(); bool initDB();
void rotate(const QString &dbName); void rotate(const QString &dbName);
bool validateUsername(const QString &username) const; bool validateUsername(const QString &username) const;
bool validatePassword(const QString &password) const; bool validatePassword(const QString &password) const;
bool validateToken(const QByteArray &token) const; bool validateToken(const QByteArray &token) const;
bool validateScopes(Types::PermissionScopes scopes) const;
void dumpDBError(const QString &message); void dumpDBError(const QString &message);
void evaluateAllowedThingsForUser();
private slots: private slots:
void onPushButtonPressed(); void onPushButtonPressed();
@ -102,7 +115,9 @@ private:
QPair<int, QString> m_pushButtonTransaction; QPair<int, QString> m_pushButtonTransaction;
}; };
} }
Q_DECLARE_METATYPE(nymeaserver::UserManager::UserError) Q_DECLARE_METATYPE(nymeaserver::UserManager::UserError)
#endif // USERMANAGER_H #endif // USERMANAGER_H

View File

@ -87,7 +87,7 @@ public:
ThingErrorItemNotFound, ThingErrorItemNotFound,
ThingErrorItemNotExecutable, ThingErrorItemNotExecutable,
ThingErrorUnsupportedFeature, ThingErrorUnsupportedFeature,
ThingErrorTimeout, ThingErrorTimeout
}; };
Q_ENUM(ThingError) Q_ENUM(ThingError)

View File

@ -69,7 +69,6 @@ public:
QVariantMap jsonMethods() const; QVariantMap jsonMethods() const;
QVariantMap jsonNotifications() const; QVariantMap jsonNotifications() const;
template<typename T> static QString enumRef(); template<typename T> static QString enumRef();
template<typename T> static QString flagRef(); template<typename T> static QString flagRef();
template<typename T> static QString objectRef(); template<typename T> static QString objectRef();

View File

@ -44,6 +44,27 @@ QString Types::scopeToString(Types::PermissionScope scope)
return metaEnum.valueToKey(scope); return metaEnum.valueToKey(scope);
} }
QStringList Types::thingIdsToStringList(const QList<ThingId> &thingIds)
{
QStringList stringList;
foreach (const ThingId &thingId, thingIds)
stringList.append(thingId.toString());
return stringList;
}
QList<ThingId> Types::thingIdsFromStringList(const QStringList &stringList)
{
QList<ThingId> thingIds;
foreach (const QString &idString, stringList) {
if (!idString.isEmpty()) {
thingIds.append(ThingId(idString));
}
}
return thingIds;
}
Types::PermissionScope Types::scopeFromString(const QString &scopeString) Types::PermissionScope Types::scopeFromString(const QString &scopeString)
{ {
QMetaEnum metaEnum = QMetaEnum::fromType<PermissionScope>(); QMetaEnum metaEnum = QMetaEnum::fromType<PermissionScope>();

View File

@ -202,10 +202,13 @@ public:
PermissionScopeNone = 0x0000, PermissionScopeNone = 0x0000,
PermissionScopeControlThings = 0x0001, PermissionScopeControlThings = 0x0001,
PermissionScopeConfigureThings = 0x0003, PermissionScopeConfigureThings = 0x0003,
PermissionScopeAccessAllThings = 0x0004,
PermissionScopeExecuteRules = 0x0010, PermissionScopeExecuteRules = 0x0010,
PermissionScopeConfigureRules = 0x0030, PermissionScopeConfigureRules = 0x0030,
PermissionScopeAdmin = 0xFFFF, PermissionScopeAdmin = 0xFFFF,
}; };
// TODO: PermissionScopeChangeUserInfos = 0x0008, // Allow to change password, remove tokens, update user information (display name, email)
Q_ENUM(PermissionScope) Q_ENUM(PermissionScope)
Q_DECLARE_FLAGS(PermissionScopes, PermissionScope) Q_DECLARE_FLAGS(PermissionScopes, PermissionScope)
Q_FLAG(PermissionScopes) Q_FLAG(PermissionScopes)
@ -215,6 +218,10 @@ public:
static QStringList scopesToStringList(PermissionScopes scopes); static QStringList scopesToStringList(PermissionScopes scopes);
static QString scopeToString(PermissionScope scope); static QString scopeToString(PermissionScope scope);
static QStringList thingIdsToStringList(const QList<ThingId> &thingIds);
static QList<ThingId> thingIdsFromStringList(const QStringList &stringList);
enum LoggingType { enum LoggingType {
LoggingTypeDiscrete, LoggingTypeDiscrete,
LoggingTypeSampled, LoggingTypeSampled,

View File

@ -11,7 +11,7 @@ isEmpty(NYMEA_VERSION) {
# define protocol versions # define protocol versions
JSON_PROTOCOL_VERSION_MAJOR=8 JSON_PROTOCOL_VERSION_MAJOR=8
JSON_PROTOCOL_VERSION_MINOR=3 JSON_PROTOCOL_VERSION_MINOR=4
JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}" JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}"
LIBNYMEA_API_VERSION_MAJOR=9 LIBNYMEA_API_VERSION_MAJOR=9
LIBNYMEA_API_VERSION_MINOR=0 LIBNYMEA_API_VERSION_MINOR=0

View File

@ -1,4 +1,4 @@
8.3 8.4
{ {
"enums": { "enums": {
"BasicType": [ "BasicType": [
@ -157,6 +157,7 @@
"PermissionScopeNone", "PermissionScopeNone",
"PermissionScopeControlThings", "PermissionScopeControlThings",
"PermissionScopeConfigureThings", "PermissionScopeConfigureThings",
"PermissionScopeAccessAllThings",
"PermissionScopeExecuteRules", "PermissionScopeExecuteRules",
"PermissionScopeConfigureRules", "PermissionScopeConfigureRules",
"PermissionScopeAdmin" "PermissionScopeAdmin"
@ -365,7 +366,8 @@
"UserErrorDuplicateUserId", "UserErrorDuplicateUserId",
"UserErrorBadPassword", "UserErrorBadPassword",
"UserErrorTokenNotFound", "UserErrorTokenNotFound",
"UserErrorPermissionDenied" "UserErrorPermissionDenied",
"UserErrorInconsistantScopes"
], ],
"ValueOperator": [ "ValueOperator": [
"ValueOperatorEquals", "ValueOperatorEquals",
@ -897,10 +899,10 @@
}, },
"permissionScope": "PermissionScopeNone", "permissionScope": "PermissionScopeNone",
"returns": { "returns": {
"items": [ "o:displayMessage": "String",
"o:items": [
"$ref:BrowserItem" "$ref:BrowserItem"
], ],
"o:displayMessage": "String",
"thingError": "$ref:ThingError" "thingError": "$ref:ThingError"
} }
}, },
@ -1046,7 +1048,8 @@
}, },
"permissionScope": "PermissionScopeNone", "permissionScope": "PermissionScopeNone",
"returns": { "returns": {
"ioConnections": "$ref:IOConnections" "o:ioConnections": "$ref:IOConnections",
"thingError": "$ref:ThingError"
} }
}, },
"Integrations.GetPluginConfiguration": { "Integrations.GetPluginConfiguration": {
@ -1969,9 +1972,23 @@
"error": "$ref:UserError" "error": "$ref:UserError"
} }
}, },
"Users.CreateUser": { "Users.ChangeUserPassword": {
"description": "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user.", "description": "Change the password for the given user. All tokens for this user will be removed in order to force all clients to log in again.",
"params": { "params": {
"newPassword": "String",
"username": "String"
},
"permissionScope": "PermissionScopeAdmin",
"returns": {
"error": "$ref:UserError"
}
},
"Users.CreateUser": {
"description": "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If the user has not the permission \"PermissionScopeAccessAllThings\", the list of things this user has access to can be defined in the \"allowedThingIds\" property. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user.",
"params": {
"o:allowedThingIds": [
"Uuid"
],
"o:displayName": "String", "o:displayName": "String",
"o:email": "String", "o:email": "String",
"o:scopes": "$ref:PermissionScopes", "o:scopes": "$ref:PermissionScopes",
@ -2003,6 +2020,17 @@
"o:userInfo": "$ref:UserInfo" "o:userInfo": "$ref:UserInfo"
} }
}, },
"Users.GetUserTokens": {
"description": "Get all the tokens for the given username.",
"params": {
"username": "String"
},
"permissionScope": "PermissionScopeAdmin",
"returns": {
"error": "$ref:UserError",
"o:tokenInfoList": "$ref:TokenInfoList"
}
},
"Users.GetUsers": { "Users.GetUsers": {
"description": "Return a list of all users in the system.", "description": "Return a list of all users in the system.",
"params": { "params": {
@ -2013,7 +2041,7 @@
} }
}, },
"Users.RemoveToken": { "Users.RemoveToken": {
"description": "Revoke access for a given token.", "description": "Revoke access for a given token. Depending on the logged in user only the own tokens can be removed. If you are logged in as admin, any token can be removed.",
"params": { "params": {
"tokenId": "Uuid" "tokenId": "Uuid"
}, },
@ -2045,8 +2073,11 @@
} }
}, },
"Users.SetUserScopes": { "Users.SetUserScopes": {
"description": "Set the permissions (scopes) for a given user.", "description": "Set the permissions (scopes) for a given user. If the user has not the permission \"PermissionScopeAccessAllThings\" the list of thing IDs this user has access to can be defined in the \"allowedThingIds\" property.",
"params": { "params": {
"o:allowedThingIds": [
"Uuid"
],
"scopes": "$ref:PermissionScopes", "scopes": "$ref:PermissionScopes",
"username": "String" "username": "String"
}, },
@ -3249,6 +3280,9 @@
"sslEnabled": "Bool" "sslEnabled": "Bool"
}, },
"UserInfo": { "UserInfo": {
"r:allowedThingIds": [
"Uuid"
],
"r:displayName": "String", "r:displayName": "String",
"r:email": "String", "r:email": "String",
"r:scopes": "$ref:PermissionScopes", "r:scopes": "$ref:PermissionScopes",

View File

@ -22,9 +22,7 @@
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <QtTest> #include "testusermanager.h"
#include "logging/logengine.h"
#include "nymeacore.h" #include "nymeacore.h"
#include "nymeatestbase.h" #include "nymeatestbase.h"
#include "usermanager/usermanager.h" #include "usermanager/usermanager.h"
@ -33,80 +31,10 @@
#include "../../utils/pushbuttonagent.h" #include "../../utils/pushbuttonagent.h"
#include "../plugins/mock/extern-plugininfo.h"
using namespace nymeaserver; using namespace nymeaserver;
class TestUsermanager: public NymeaTestBase
{
Q_OBJECT
public:
TestUsermanager(QObject* parent = nullptr);
private slots:
void initTestCase();
void init();
void loginValidation_data();
void loginValidation();
void createUser();
void authenticate();
/*
Cases for push button auth:
Case 1: regular pushbutton
- alice sends Users.RequestPushButtonAuth, gets "OK" back (if push button hardware is available)
- alice pushes the hardware button and gets a notification on jsonrpc containing the token for local auth
*/
void authenticatePushButton();
/*
Case 2: if we have an attacker in the network, he could try to call requestPushButtonAuth and
hope someone would eventually press the button and give him a token. In order to prevent this,
any previous attempt for a push button auth needs to be cancelled when a new request comes in:
* Mallory does RequestPushButtonAuth, gets OK back
* Alice does RequestPushButtonAuth,
* Mallory receives a "PushButtonFailed" notification
* Alice receives OK
* Alice presses the hardware button
* Alice reveices a notification with token, mallory receives nothing
Case 3: Mallory tries to hijack it back again
* Mallory does RequestPushButtonAuth, gets OK back
* Alice does RequestPusButtonAuth,
* Alice gets ok reply, Mallory gets failed notification
* Mallory quickly does RequestPushButtonAuth again to win the fight
* Alice gets failed notification and can instruct the user to _not_ press the button now until procedure is restarted
*/
void authenticatePushButtonAuthInterrupt();
void authenticatePushButtonAuthConnectionDrop();
void createDuplicateUser();
void getTokens();
void removeToken();
void unauthenticatedCallAfterTokenRemove();
void changePassword();
void authenticateAfterPasswordChangeOK();
void authenticateAfterPasswordChangeFail();
void getUserInfo();
private:
// m_apiToken is in testBase
QUuid m_tokenId;
};
TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent) TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
{ {
QCoreApplication::instance()->setOrganizationName("nymea-test"); QCoreApplication::instance()->setOrganizationName("nymea-test");
@ -116,11 +44,11 @@ void TestUsermanager::initTestCase()
{ {
NymeaDBusService::setBusType(QDBusConnection::SessionBus); NymeaDBusService::setBusType(QDBusConnection::SessionBus);
NymeaTestBase::initTestCase("*.debug=false\n" NymeaTestBase::initTestCase("*.debug=false\n"
"Application.debug=true\n" "Application.debug=true\n"
"Tests.debug=true\n" "Tests.debug=true\n"
"UserManager.debug=true\n" "UserManager.debug=true\n"
"PushButtonAgent.debug=true\n" "PushButtonAgent.debug=true\n"
"MockDevice.debug=true"); "MockDevice.debug=true");
} }
void TestUsermanager::init() void TestUsermanager::init()
@ -357,7 +285,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
// Create a new clientId for alice and connect it to the server // Create a new clientId for alice and connect it to the server
QUuid aliceId = QUuid::createUuid(); QUuid aliceId = QUuid::createUuid();
m_mockTcpServer->clientConnected(aliceId); emit m_mockTcpServer->clientConnected(aliceId);
m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait(); if (clientSpy.count() == 0) clientSpy.wait();
@ -368,12 +296,12 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
// Disconnect alice // Disconnect alice
m_mockTcpServer->clientDisconnected(aliceId); emit m_mockTcpServer->clientDisconnected(aliceId);
// Now try with bob // Now try with bob
// Create a new clientId for bob and connect it to the server // Create a new clientId for bob and connect it to the server
QUuid bobId = QUuid::createUuid(); QUuid bobId = QUuid::createUuid();
m_mockTcpServer->clientConnected(bobId); emit m_mockTcpServer->clientConnected(bobId);
clientSpy.clear(); clientSpy.clear();
m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait(); if (clientSpy.count() == 0) clientSpy.wait();
@ -495,9 +423,8 @@ void TestUsermanager::authenticateAfterPasswordChangeFail()
QVERIFY2(disconnectedSpy.count() == 1, "Connection should have dropped"); QVERIFY2(disconnectedSpy.count() == 1, "Connection should have dropped");
QTest::qWait(3200); QTest::qWait(3200);
m_mockTcpServer->clientConnected(m_clientId); emit m_mockTcpServer->clientConnected(m_clientId);
injectAndWait("JSONRPC.Hello"); injectAndWait("JSONRPC.Hello");
} }
void TestUsermanager::getUserInfo() void TestUsermanager::getUserInfo()
@ -505,14 +432,9 @@ void TestUsermanager::getUserInfo()
authenticate(); authenticate();
QVariant response = injectAndWait("Users.GetUserInfo"); QVariant response = injectAndWait("Users.GetUserInfo");
QCOMPARE(response.toMap().value("status").toString(), QString("success")); QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QVariantMap userInfoMap = response.toMap().value("params").toMap().value("userInfo").toMap(); QVariantMap userInfoMap = response.toMap().value("params").toMap().value("userInfo").toMap();
QCOMPARE(userInfoMap.value("username").toString(), QString("valid@user.test")); QCOMPARE(userInfoMap.value("username").toString(), QString("valid@user.test"));
} }
void TestUsermanager::unauthenticatedCallAfterTokenRemove() void TestUsermanager::unauthenticatedCallAfterTokenRemove()
@ -530,9 +452,270 @@ void TestUsermanager::unauthenticatedCallAfterTokenRemove()
QVERIFY2(spy.count() == 1, "Connection should be terminated!"); QVERIFY2(spy.count() == 1, "Connection should be terminated!");
QTest::qWait(3200); QTest::qWait(3200);
m_mockTcpServer->clientConnected(m_clientId); emit m_mockTcpServer->clientConnected(m_clientId);
injectAndWait("JSONRPC.Hello"); injectAndWait("JSONRPC.Hello");
} }
#include "testusermanager.moc" void TestUsermanager::testScopeConsitancy_data()
{
QTest::addColumn<QList<Types::PermissionScope>>("scopes");
QTest::addColumn<QString>("error");
QTest::newRow("valid: admin")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeAdmin)
<< "UserErrorNoError";
QTest::newRow("valid: none")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeNone)
<< "UserErrorNoError";
QTest::newRow("valid: only control, not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeAccessAllThings)
<< "UserErrorNoError";
QTest::newRow("valid: only control, not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeConfigureThings
<< Types::PermissionScopeAccessAllThings)
<< "UserErrorNoError";
QTest::newRow("valid: only control, all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeAccessAllThings)
<< "UserErrorNoError";
QTest::newRow("valid: control things/rules, all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeAccessAllThings
<< Types::PermissionScopeExecuteRules)
<< "UserErrorNoError";
QTest::newRow("valid: only execute rules")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeAccessAllThings
<< Types::PermissionScopeExecuteRules)
<< "UserErrorNoError";
QTest::newRow("invalid: missing control and all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeConfigureThings)
<< "UserErrorInconsistantScopes";
QTest::newRow("invalid: control/configure things. not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeConfigureThings)
<< "UserErrorInconsistantScopes";
QTest::newRow("invalid: only execute rules, not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeExecuteRules)
<< "UserErrorInconsistantScopes";
QTest::newRow("invalid: only configure rules")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeConfigureRules)
<< "UserErrorInconsistantScopes";
QTest::newRow("invalid: configure and execute rules, not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeExecuteRules
<< Types::PermissionScopeConfigureRules)
<< "UserErrorInconsistantScopes";
QTest::newRow("invalid: control things/rules, not all things")
<< (QList<Types::PermissionScope>()
<< Types::PermissionScopeControlThings
<< Types::PermissionScopeExecuteRules)
<< "UserErrorInconsistantScopes";
}
void TestUsermanager::testScopeConsitancy()
{
QFETCH(QList<Types::PermissionScope>, scopes);
QFETCH(QString, error);
authenticate();
QVariant response = injectAndWait("Users.GetUserInfo");
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QVariantMap userInfoMap = response.toMap().value("params").toMap().value("userInfo").toMap();
QCOMPARE(userInfoMap.value("username").toString(), QString("valid@user.test"));
QMetaEnum metaEnum = QMetaEnum::fromType<Types::PermissionScope>();
QStringList scopesList;
foreach (Types::PermissionScope scope, scopes)
scopesList.append(metaEnum.valueToKey(scope));
// Now try to edit with the given scopes
QVariantMap params;
params.insert("username", userInfoMap.value("username").toString());
params.insert("scopes", scopesList);
response = injectAndWait("Users.SetUserScopes", params);
QCOMPARE(response.toMap().value("status").toString(), QString("success"));
QCOMPARE(response.toMap().value("params").toMap().value("error").toString(), error);
}
void TestUsermanager::testRestrictedThingAccess()
{
// Add 2 mock things
ThingId thingIdOne;
ThingId thingIdTwo;
QString usernameAdmin = "admin";
QString passwordAdmin = "Bla1234*";
QString usernameGuest = "guest";
QString passwordGuest = "Bla1234+";
QVariant response;
QVariantList thingParams;
QVariantMap params;
injectAndWait("JSONRPC.Hello");
// Create admin user
params.clear();
params.insert("username", usernameAdmin);
params.insert("password", passwordAdmin);
response = injectAndWait("JSONRPC.CreateUser", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Error creating user");
QVERIFY2(response.toMap().value("params").toMap().value("error").toString() == "UserErrorNoError", "Error creating user");
// Authenticate admin user
params.clear();
params.insert("username", usernameAdmin);
params.insert("password", passwordAdmin);
params.insert("deviceName", "autotests");
response = injectAndWait("JSONRPC.Authenticate", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating");
QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating");
m_adminToken = response.toMap().value("params").toMap().value("token").toByteArray();
// Use the admin token for now
m_apiToken = m_adminToken;
// Add thing one
QVariantMap httpportParamOne;
httpportParamOne.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
httpportParamOne.insert("value", m_mockThing1Port - 1);
thingParams << httpportParamOne;
params.clear();
params.insert("thingClassId", mockThingClassId);
params.insert("name", "Test thing available for all users");
params.insert("thingParams", thingParams);
response = injectAndWait("Integrations.AddThing", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorNoError));
thingIdOne = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
// Add thing two
QVariantMap httpportParamTwo;
httpportParamTwo.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
httpportParamTwo.insert("value", m_mockThing1Port - 2);
thingParams.clear();
thingParams << httpportParamTwo;
params.clear();
params.insert("thingClassId", mockThingClassId);
params.insert("name", "Test thing available for all users");
params.insert("thingParams", thingParams);
response = injectAndWait("Integrations.AddThing", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorNoError));
thingIdTwo = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
// Create guest user
QStringList scopes;
scopes << "PermissionScopeControlThings";
QVariantList allowedThingIds;
allowedThingIds << thingIdTwo;
params.clear();
params.insert("username", usernameGuest);
params.insert("password", passwordGuest);
params.insert("scopes", scopes);
params.insert("allowedThingIds", allowedThingIds);
response = injectAndWait("Users.CreateUser", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Error creating user");
QVERIFY2(response.toMap().value("params").toMap().value("error").toString() == "UserErrorNoError", "Error creating user");
response = injectAndWait("Integrations.GetThings");
QVariantList things = response.toMap().value("params").toMap().value("things").toList();
//qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(things).toJson(QJsonDocument::Indented));
QVERIFY2(things.count() >= 2, "Expected to get 2 or more things as admin");
// Everything set up, now authenticate as guest
// Authenticate guest user
params.clear();
params.insert("username", usernameGuest);
params.insert("password", passwordGuest);
params.insert("deviceName", "autotests");
response = injectAndWait("JSONRPC.Authenticate", params);
QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating");
QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating");
m_guestToken = response.toMap().value("params").toMap().value("token").toByteArray();
// Use the admin token for now
m_apiToken = m_guestToken;
// Try to access restricted thing
response = injectAndWait("Integrations.GetThings");
verifyError(response, "thingError", enumValueName(Thing::ThingErrorNoError));
things = response.toMap().value("params").toMap().value("things").toList();
QVERIFY2(things.count() == 1, "Expected to get exactly 1 things as guest");
// GetThings (access)
params.clear();
params.insert("thingId", thingIdTwo);
response = injectAndWait("Integrations.GetThings", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorNoError));
// GetThings (no access)
params.clear();
params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.GetThings", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// 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 (no access)
params.clear();
params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.BrowseThing", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// GetBrowserItem (no access)
params.clear();
params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.GetBrowserItem", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// Clean up
UserManager *userManager = NymeaCore::instance()->userManager();
foreach (const UserInfo &userInfo, userManager->users()) {
qCDebug(dcTests()) << "Removing user" << userInfo.username();
userManager->removeUser(userInfo.username());
}
userManager->removeUser("");
}
QTEST_MAIN(TestUsermanager) QTEST_MAIN(TestUsermanager)

View File

@ -0,0 +1,125 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2025, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef TESTUSERMANAGER_H
#define TESTUSERMANAGER_H
#include <QtTest>
#include "nymeatestbase.h"
using namespace nymeaserver;
class TestUsermanager: public NymeaTestBase
{
Q_OBJECT
public:
TestUsermanager(QObject* parent = nullptr);
private slots:
void initTestCase();
void init();
void loginValidation_data();
void loginValidation();
void createUser();
void authenticate();
/*
Cases for push button auth:
Case 1: regular pushbutton
- alice sends Users.RequestPushButtonAuth, gets "OK" back (if push button hardware is available)
- alice pushes the hardware button and gets a notification on jsonrpc containing the token for local auth
*/
void authenticatePushButton();
/*
Case 2: if we have an attacker in the network, he could try to call requestPushButtonAuth and
hope someone would eventually press the button and give him a token. In order to prevent this,
any previous attempt for a push button auth needs to be cancelled when a new request comes in:
* Mallory does RequestPushButtonAuth, gets OK back
* Alice does RequestPushButtonAuth,
* Mallory receives a "PushButtonFailed" notification
* Alice receives OK
* Alice presses the hardware button
* Alice reveices a notification with token, mallory receives nothing
Case 3: Mallory tries to hijack it back again
* Mallory does RequestPushButtonAuth, gets OK back
* Alice does RequestPusButtonAuth,
* Alice gets ok reply, Mallory gets failed notification
* Mallory quickly does RequestPushButtonAuth again to win the fight
* Alice gets failed notification and can instruct the user to _not_ press the button now until procedure is restarted
*/
void authenticatePushButtonAuthInterrupt();
void authenticatePushButtonAuthConnectionDrop();
void createDuplicateUser();
void getTokens();
void removeToken();
void unauthenticatedCallAfterTokenRemove();
void changePassword();
void authenticateAfterPasswordChangeOK();
void authenticateAfterPasswordChangeFail();
void getUserInfo();
void testScopeConsitancy_data();
void testScopeConsitancy();
void testRestrictedThingAccess();
private:
// m_apiToken is in testBase
QUuid m_tokenId;
void authenticateTestuser(const QString &username);
QString m_usernameAdmin = "admin";
QString m_usernameGuest = "guest";
QByteArray m_adminToken;
QByteArray m_guestToken;
};
#endif // TESTUSERMANAGER_H

View File

@ -3,7 +3,8 @@ include(../autotests.pri)
TARGET = nymeatestusermanager TARGET = nymeatestusermanager
HEADERS += ../../utils/pushbuttonagent.h HEADERS += ../../utils/pushbuttonagent.h \
testusermanager.h
SOURCES += testusermanager.cpp \ SOURCES += testusermanager.cpp \
../../utils/pushbuttonagent.cpp ../../utils/pushbuttonagent.cpp