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 "ruleengine/ruleengine.h"
#include "nymeacore.h"
#include "usermanager/usermanager.h"
#include <QDebug>
#include <QJsonDocument>
#include <QCryptographicHash>
@ -128,12 +131,12 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
description = "Add a new thing to the system. "
"Only things with a setupMethod of SetupMethodJustAdd can be added this way. "
"For things with a setupMethod different than SetupMethodJustAdd, use PairThing. "
"Things with CreateMethodJustAdd require all parameters to be supplied here. "
"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 "
"may be overridden by supplying thingParams.";
"Only things with a setupMethod of SetupMethodJustAdd can be added this way. "
"For things with a setupMethod different than SetupMethodJustAdd, use PairThing. "
"Things with CreateMethodJustAdd require all parameters to be supplied here. "
"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 "
"may be overridden by supplying thingParams.";
params.insert("o:thingClassId", enumValueName(Uuid));
params.insert("name", enumValueName(String));
params.insert("o:thingDescriptorId", enumValueName(Uuid));
@ -145,23 +148,23 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
description = "Pair a new thing. "
"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 "
"are required:\n"
"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"
"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 "
"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 "
"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 "
"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 "
"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"
"Once the login procedure has completed, the application shall proceed with ConfirmPairing, providing the results of the pairing "
"procedure.";
"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 "
"are required:\n"
"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"
"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 "
"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 "
"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 "
"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 "
"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"
"Once the login procedure has completed, the application shall proceed with ConfirmPairing, providing the results of the pairing "
"procedure.";
params.insert("o:thingClassId", enumValueName(Uuid));
params.insert("o:name", enumValueName(String));
params.insert("o:thingDescriptorId", enumValueName(Uuid));
@ -177,9 +180,9 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
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\" "
"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 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 "
"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("o:username", enumValueName(String));
params.insert("o:secret", enumValueName(String));
@ -197,10 +200,10 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
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 "
"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 "
"existing things and might be filtered in cases where only unknown things are of interest.";
"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 "
"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.";
params.insert("thingClassId", enumValueName(Uuid));
params.insert("o:discoveryParams", objectRef<ParamList>());
returns.insert("thingError", enumRef<Thing::ThingError>());
@ -311,26 +314,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
description = "Browse a thing. "
"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 "
"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 "
"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.";
"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 "
"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 "
"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.";
params.insert("thingId", enumValueName(Uuid));
params.insert("o:itemId", enumValueName(String));
returns.insert("thingError", enumRef<Thing::ThingError>());
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);
params.clear(); returns.clear();
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 "
"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 "
"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.";
"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"
"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 "
"message to the user should be prepared for empty, but also longer strings.";
params.insert("thingId", enumValueName(Uuid));
params.insert("o:itemId", enumValueName(String));
returns.insert("thingError", enumRef<Thing::ThingError>());
@ -374,7 +377,8 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
params.clear(); returns.clear();
description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId.";
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);
params.clear(); returns.clear();
@ -442,7 +446,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
connect(m_thingManager, &ThingManager::eventTriggered, this, [this](const Event &event){
QVariantMap params;
params.insert("event", pack(event));
emit EventTriggered(params);
emit EventTriggered(params, event.thingId());
});
params.clear(); returns.clear();
@ -515,6 +519,21 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
hash = QCryptographicHash::hash(QJsonDocument::fromVariant(pluginList).toJson(), QCryptographicHash::Md5).toHex();
m_cacheHashes.insert("GetPlugins", hash);
});
connect(NymeaCore::instance()->userManager(), &UserManager::userThingRestrictionsChanged, this, [this](const UserInfo &userInfo, const ThingId &thingId, bool accessGranted){
if (accessGranted) {
QVariantMap params;
params.insert("thing", pack(m_thingManager->findConfiguredThing(thingId)));
emit ThingAdded(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been granted.";
} else {
QVariantMap params;
params.insert("thingId", thingId);
emit ThingRemoved(params, userInfo);
qCDebug(dcJsonRpc()) << "Notify user" << userInfo.username() << "that the permission to thing with ID" << thingId.toString() << "has been dropped.";
}
});
}
QString IntegrationsHandler::name() const
@ -527,7 +546,7 @@ QHash<QString, QString> IntegrationsHandler::cacheHashes() const
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)
QVariantList vendors;
@ -541,7 +560,7 @@ JsonReply* IntegrationsHandler::GetVendors(const QVariantMap &params, const Json
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;
QVariantList thingClasses;
@ -600,13 +619,13 @@ JsonReply *IntegrationsHandler::DiscoverThings(const QVariantMap &params, const
}
reply->setData(returns);
reply->finished();
emit reply->finished();
});
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)
QVariantList plugins;
@ -640,7 +659,7 @@ JsonReply *IntegrationsHandler::GetPluginConfiguration(const QVariantMap &params
return createReply(returns);
}
JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params)
JsonReply *IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params)
{
QVariantMap returns;
PluginId pluginId = PluginId(params.value("pluginId").toString());
@ -650,7 +669,7 @@ JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap &params
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());
QString thingName = params.value("name").toString();
@ -667,7 +686,7 @@ JsonReply* IntegrationsHandler::AddThing(const QVariantMap &params, const JsonCo
QVariantMap returns;
returns.insert("thingError", enumValueName<Thing::ThingError>(Thing::ThingErrorMissingParameter));
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
return jsonReply;
}
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());
}
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -732,7 +751,7 @@ JsonReply *IntegrationsHandler::PairThing(const QVariantMap &params, const JsonC
}
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -759,39 +778,74 @@ JsonReply *IntegrationsHandler::ConfirmPairing(const QVariantMap &params)
returns.insert("thingId", info->thingId().toString());
}
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
}
JsonReply* IntegrationsHandler::GetThings(const QVariantMap &params, const JsonContext &context) const
JsonReply *IntegrationsHandler::GetThings(const QVariantMap &params, const JsonContext &context) const
{
QVariantMap returns;
QVariantList things;
if (params.contains("thingId")) {
Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString()));
if (!thing) {
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;
if (NymeaCore::instance()->userManager()->hasRestrictedThingAccess(context.token())) {
// Restricted things access
QList<ThingId> allowedThingIds = NymeaCore::instance()->userManager()->getAllowedThingIdsForToken(context.token());
if (params.contains("thingId")) {
ThingId thingId(params.value("thingId").toString());
Thing *thing = m_thingManager->findConfiguredThing(thingId);
if (!thing || !allowedThingIds.contains(thingId)) {
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()) {
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 {
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;
// Unrestricted things access
if (params.contains("thingId")) {
Thing *thing = m_thingManager->findConfiguredThing(ThingId(params.value("thingId").toString()));
if (!thing) {
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("things", things);
return createReply(returns);
@ -819,13 +873,11 @@ JsonReply *IntegrationsHandler::ReconfigureThing(const QVariantMap &params, cons
}
connect(info, &ThingSetupInfo::finished, jsonReply, [info, jsonReply, locale](){
QVariantMap returns;
returns.insert("thingError", enumValueName<Thing::ThingError>(info->status()));
returns.insert("displayMessage", info->translatedDisplayMessage(locale));
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -843,7 +895,7 @@ JsonReply *IntegrationsHandler::EditThing(const QVariantMap &params)
return createReply(statusToReply(status));
}
JsonReply* IntegrationsHandler::RemoveThing(const QVariantMap &params)
JsonReply *IntegrationsHandler::RemoveThing(const QVariantMap &params)
{
QVariantMap returns;
ThingId thingId = ThingId(params.value("thingId").toString());
@ -898,7 +950,7 @@ JsonReply *IntegrationsHandler::SetStateFilter(const QVariantMap &params)
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 translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -908,7 +960,7 @@ JsonReply* IntegrationsHandler::GetEventTypes(const QVariantMap &params, const J
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 translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -918,7 +970,7 @@ JsonReply* IntegrationsHandler::GetActionTypes(const QVariantMap &params, const
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 translatedThingClass = m_thingManager->translateThingClass(thingClass, context.locale());
@ -928,28 +980,34 @@ JsonReply* IntegrationsHandler::GetStateTypes(const QVariantMap &params, const J
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()));
if (!thing) {
ThingId thingId(params.value("thingId").toString());
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));
StateTypeId stateTypeId = StateTypeId(params.value("stateTypeId").toString());
if (!thing->hasState(stateTypeId)) {
if (!thing->hasState(stateTypeId))
return createReply(statusToReply(Thing::ThingErrorStateTypeNotFound));
}
QVariantMap returns = statusToReply(Thing::ThingErrorNoError);
returns.insert("value", thing->state(stateTypeId).value());
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()));
if (!thing) {
ThingId thingId(params.value("thingId").toString());
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));
}
QVariantMap returns = statusToReply(Thing::ThingErrorNoError);
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
{
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();
JsonReply *jsonReply = createAsyncReply("BrowseThing");
@ -968,15 +1029,16 @@ JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const Jso
QVariantMap returns = statusToReply(result->status());
QVariantList list;
foreach (const BrowserItem &item, result->items()) {
foreach (const BrowserItem &item, result->items())
list.append(packBrowserItem(item));
}
returns.insert("items", list);
if (!result->displayMessage().isEmpty()) {
if (!result->displayMessage().isEmpty())
returns.insert("displayMessage", result->translatedDisplayMessage(context.locale()));
}
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -984,10 +1046,11 @@ JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap &params, const Jso
JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const JsonContext &context) const
{
QVariantMap returns;
ThingId thingId = ThingId(params.value("thingId").toString());
QString itemId = params.value("itemId").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();
JsonReply *jsonReply = createAsyncReply("GetBrowserItem");
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()));
}
jsonReply->setData(params);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -1009,6 +1072,9 @@ JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap &params, const
JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const JsonContext &context)
{
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());
ParamList actionParams = unpack<ParamList>(params.value("params"));
QLocale locale = context.locale();
@ -1026,7 +1092,7 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const J
data.insert("displayMessage", info->translatedDisplayMessage(locale));
}
jsonReply->setData(data);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -1035,6 +1101,9 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const J
JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context)
{
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();
BrowserAction action(thingId, itemId);
@ -1048,7 +1117,7 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, co
data.insert("displayMessage", info->translatedDisplayMessage(context.locale()));
}
jsonReply->setData(data);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -1057,6 +1126,9 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap &params, co
JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context)
{
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();
ActionTypeId actionTypeId = ActionTypeId(params.value("actionTypeId").toString());
ParamList paramList = unpack<ParamList>(params.value("params"));
@ -1072,19 +1144,22 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &para
data.insert("displayMessage", info->translatedDisplayMessage(context.locale()));
}
jsonReply->setData(data);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
}
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params)
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params, const JsonContext &context)
{
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);
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);
}
@ -1153,28 +1228,28 @@ void IntegrationsHandler::thingStateChanged(Thing *thing, const QUuid &stateType
params.insert("minValue", minValue);
params.insert("maxValue", maxValue);
params.insert("possibleValues", possibleValues);
emit StateChanged(params);
emit StateChanged(params, thing->id());
}
void IntegrationsHandler::thingRemovedNotification(const ThingId &thingId)
{
QVariantMap params;
params.insert("thingId", thingId);
emit ThingRemoved(params);
emit ThingRemoved(params, thingId);
}
void IntegrationsHandler::thingAddedNotification(Thing *thing)
{
QVariantMap params;
params.insert("thing", pack(thing));
emit ThingAdded(params);
emit ThingAdded(params, thing->id());
}
void IntegrationsHandler::thingChangedNotification(Thing *thing)
{
QVariantMap params;
params.insert("thing", pack(thing));
emit ThingChanged(params);
emit ThingChanged(params, thing->id());
}
void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId, const ParamTypeId &paramTypeId, const QVariant &value)
@ -1183,7 +1258,7 @@ void IntegrationsHandler::thingSettingChangedNotification(const ThingId &thingId
params.insert("thingId", thingId);
params.insert("paramTypeId", paramTypeId.toString());
params.insert("value", value);
emit ThingSettingChanged(params);
emit ThingSettingChanged(params, thingId);
}
QVariantMap IntegrationsHandler::statusToReply(Thing::ThingError status) const

View File

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

View File

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

View File

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

View File

@ -42,12 +42,13 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
QString description;
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("password", enumValueName(String));
params.insert("o:email", enumValueName(String));
params.insert("o:displayName", enumValueName(String));
params.insert("o:scopes", flagRef<Types::PermissionScopes>());
params.insert("o:allowedThingIds", QVariantList() << enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>());
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.";
params.insert("newPassword", enumValueName(String));
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();
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.";
returns.insert("o:tokenInfoList", objectRef<TokenInfoList>());
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("GetTokens", description, params, returns);
registerMethod("GetTokens", description, params, returns); // TODO: PermissionScopeChangeUserInfos
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));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("RemoveToken", description, params, returns);
registerMethod("RemoveToken", description, params, returns); // TODO: PermissionScopeChangeUserInfos
params.clear(); returns.clear();
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);
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("scopes", flagRef<Types::PermissionScopes>());
params.insert("o:allowedThingIds", QVariantList() << enumValueName(Uuid));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("SetUserScopes", description, params, returns);
@ -141,7 +158,6 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
params.insert("username", username);
emit UserRemoved(params);
});
}
QString UsersHandler::name() const
@ -157,8 +173,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
QString displayName = params.value("displayName").toString();
QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList();
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;
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)
{
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
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?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
QString newPassword = params.value("newPassword").toString();
@ -187,52 +207,82 @@ JsonReply *UsersHandler::ChangePassword(const QVariantMap &params, const JsonCon
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
UserManager::UserError status = m_userManager->changePassword(tokenInfo.username(), newPassword);
ret.insert("error", enumValueName<UserManager::UserError>(status));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(status));
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)
{
Q_UNUSED(params)
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
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?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
UserInfo userInfo = m_userManager->userInfo(tokenInfo.username());
ret.insert("userInfo", pack(userInfo));
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret);
returns.insert("userInfo", pack(userInfo));
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(returns);
}
JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext &context)
{
Q_UNUSED(params)
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
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?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken);
@ -242,52 +292,87 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext
foreach (const TokenInfo &tokenInfo, tokens) {
retList << pack(tokenInfo);
}
ret.insert("tokenInfoList", retList);
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
return createReply(ret);
returns.insert("tokenInfoList", retList);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorNoError));
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)
{
QVariantMap ret;
QVariantMap returns;
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection.";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
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?";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
QUuid tokenId = params.value("tokenId").toUuid();
TokenInfo tokenToRemove = m_userManager->tokenInfo(tokenId);
if (tokenToRemove.id().isNull()) {
qCWarning(dcJsonRpc()) << "Token with ID" << tokenId << "not found";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorTokenNotFound));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorTokenNotFound));
return createReply(returns);
}
TokenInfo currentTokenInfo = m_userManager->tokenInfo(currentToken);
if (currentTokenInfo.username() != tokenToRemove.username()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from another user!";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
qCDebug(dcJsonRpc()) << "Removing token" << tokenId << "for user" << currentTokenInfo.username();
UserManager::UserError error = m_userManager->removeToken(tokenId);
ret.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(ret);
returns.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(returns);
}
JsonReply *UsersHandler::GetUsers(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap reply;
reply.insert("users", pack(m_userManager->users()));
return createReply(reply);
@ -306,9 +391,20 @@ JsonReply *UsersHandler::RemoveUser(const QVariantMap &params, const JsonContext
JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonContext &context)
{
Q_UNUSED(context)
qCWarning(dcJsonRpc()) << params;
QString username = params.value("username").toString();
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;
returns.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(returns);
@ -316,7 +412,7 @@ JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonCont
JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContext &context)
{
QVariantMap ret;
QVariantMap returns;
TokenInfo callingTokenInfo = m_userManager->tokenInfo(context.token());
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)) {
ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied));
return createReply(ret);
returns.insert("error", enumValueName(UserManager::UserErrorPermissionDenied));
return createReply(returns);
}
UserInfo changedUserInfo = m_userManager->userInfo(username);
@ -347,8 +443,8 @@ JsonReply *UsersHandler::SetUserInfo(const QVariantMap &params, const JsonContex
displayName = changedUserInfo.displayName();
}
UserManager::UserError status = m_userManager->setUserInfo(username, email, displayName);
ret.insert("error", enumValueName(status));
return createReply(ret);
returns.insert("error", enumValueName(status));
return createReply(returns);
}
}

View File

@ -43,8 +43,10 @@ public:
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
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 *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 *GetUsers(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap &params, const JsonContext &context);

View File

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

View File

@ -79,6 +79,16 @@ void UserInfo::setScopes(Types::PermissionScopes 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
{
return QVariant::fromValue(at(index));

View File

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

View File

@ -63,7 +63,6 @@
*/
#include "usermanager.h"
#include "nymeasettings.h"
#include "loggingcategories.h"
#include "pushbuttondbusservice.h"
#include "nymeacore.h"
@ -96,7 +95,7 @@ UserManager::UserManager(const QString &dbName, QObject *parent):
if (!initDB()) {
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());
if (!initDB()) {
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();
return users;
}
while (resultQuery.next()) {
UserInfo info = UserInfo(resultQuery.value("username").toString());
info.setEmail(resultQuery.value("email").toString());
info.setDisplayName(resultQuery.value("displayName").toString());
info.setScopes(Types::scopesFromStringList(resultQuery.value("scopes").toString().split(',')));
info.setAllowedThingIds(Types::thingIdsFromStringList(resultQuery.value("allowedThingIds").toString().split(',')));
users.append(info);
}
return users;
}
/*! 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)) {
qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username;
@ -164,26 +165,53 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
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);
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
checkForDuplicateUserQuery.addBindValue(username.toLower());
checkForDuplicateUserQuery.exec();
if (checkForDuplicateUserQuery.first()) {
qCWarning(dcUserManager) << "Username already in use";
qCWarning(dcUserManager) << "Username" << username << "already in use";
return UserErrorDuplicateUserId;
}
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
static QRegularExpression bracketsRe("[{}]");
QByteArray salt = QUuid::createUuid().toString().remove(bracketsRe).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) VALUES(?, ?, ?, ?, ?, ?);");
query.addBindValue(username.toLower());
query.addBindValue(email);
query.addBindValue(displayName);
query.addBindValue(QString::fromUtf8(hashedPassword));
query.addBindValue(QString::fromUtf8(salt));
query.addBindValue(Types::scopesToStringList(scopes).join(','));
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds)"
"VALUES(:username, :email, :displayName, :password, :salt, :scopes, :allowedThingIds);");
query.bindValue(":username", username.toLower());
query.bindValue(":email", email);
query.bindValue(":displayName", displayName);
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();
if (query.lastError().type() != QSqlError::NoError) {
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
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
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);
if (!updatePasswordQuery.exec(updatePasswordQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << updatePasswordQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
updatePasswordQuery.prepare("UPDATE users SET password = :password, salt = :salt WHERE lower(username) = :username;");
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;
}
@ -244,7 +273,7 @@ UserManager::UserError UserManager::changePassword(const QString &username, cons
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);
if (!dropUserQuery.exec(dropUserQueryString)) {
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;
}
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 allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(',');
qCDebug(dcUserManager()) << "Updating scopes of user" << username << "Scopes:" << scopes << "Allowed things:" << allowedThingIds;
QSqlQuery setScopesQuery(m_db);
setScopesQuery.prepare("UPDATE users SET scopes = ? WHERE username = ?");
setScopesQuery.addBindValue(scopesString);
setScopesQuery.addBindValue(username);
setScopesQuery.exec();
if (setScopesQuery.lastError().type() != QSqlError::NoError) {
setScopesQuery.prepare("UPDATE users SET scopes = :scopes, allowedThingIds = :allowedThingIds WHERE username = :username;");
setScopesQuery.bindValue(":username", username);
setScopesQuery.bindValue(":scopes", scopesString);
setScopesQuery.bindValue(":allowedThingIds", allowedThingIdsString);
if (!setScopesQuery.exec()) {
qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText();
return UserErrorBackendError;
}
emit userChanged(username);
// Notify after updating the user information
UserInfo ui = userInfo(username);
foreach (const ThingId &thingId, thingsAppeared)
emit userThingRestrictionsChanged(ui, thingId, true);
foreach (const ThingId &thingId, thingsDisappeared)
emit userThingRestrictionsChanged(ui, thingId, false);
return UserErrorNoError;
}
UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName)
{
QSqlQuery query(m_db);
query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;");
query.addBindValue(email);
query.addBindValue(displayName);
query.addBindValue(username);
query.prepare("UPDATE users SET email = :email, displayName = :displayName WHERE username = :username;");
query.bindValue(":email", email);
query.bindValue(":displayName", displayName);
query.bindValue(":username", username);
query.exec();
if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
@ -315,13 +409,14 @@ QByteArray UserManager::authenticate(const QString &username, const QString &pas
}
QSqlQuery passwordQuery(m_db);
passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = ?;");
passwordQuery.addBindValue(username.toLower());
passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = :username;");
passwordQuery.bindValue(":username", username.toLower());
passwordQuery.exec();
if (!passwordQuery.first()) {
qCWarning(dcUserManager) << "No such username" << username;
return QByteArray();
}
QByteArray salt = passwordQuery.value("salt").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();
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);
if (!storeTokenQuery.exec(storeTokenQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << storeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
storeTokenQuery.prepare("INSERT INTO tokens (id, username, token, creationdate, devicename)"
"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();
}
@ -391,18 +488,16 @@ void UserManager::cancelPushButtonAuth(int transactionId)
*/
UserInfo UserManager::userInfo(const QString &username) const
{
QString getUserQueryString = QString("SELECT * FROM users WHERE lower(username) = \"%1\";")
.arg(username);
QSqlQuery getUserQuery(m_db);
if (!getUserQuery.exec(getUserQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
getUserQuery.prepare("SELECT * FROM users WHERE lower(username) = :username;");
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();
}
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();
}
@ -413,7 +508,7 @@ UserInfo UserManager::userInfo(const QString &username) const
userInfo.setEmail(getUserQuery.value("email").toString());
userInfo.setDisplayName(getUserQuery.value("displayName").toString());
userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(',')));
userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(',')));
return userInfo;
}
@ -422,8 +517,8 @@ QList<TokenInfo> UserManager::tokens(const QString &username) const
QList<TokenInfo> ret;
QSqlQuery query(m_db);
query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = ?;");
query.addBindValue(username.toLower());
query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = :username;");
query.bindValue(":username", username.toLower());
query.exec();
if (m_db.lastError().type() != QSqlError::NoError) {
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();
}
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
QSqlQuery getTokenQuery(m_db);
if (!getTokenQuery.exec(getTokenQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
getTokenQuery.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = :token;");
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();
}
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();
}
@ -465,18 +559,16 @@ TokenInfo UserManager::tokenInfo(const QByteArray &token) 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);
if (!getTokenQuery.exec(getTokenQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
getTokenQuery.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = :id;");
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();
}
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();
}
@ -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. */
UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
{
QString removeTokenQueryString = QString("DELETE FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
QSqlQuery removeTokenQuery(m_db);
if (!removeTokenQuery.exec(removeTokenQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << removeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
removeTokenQuery.prepare("DELETE FROM tokens WHERE id = :id;");
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;
}
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;
}
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;
}
@ -518,27 +611,65 @@ bool UserManager::verifyToken(const QByteArray &token)
qCWarning(dcUserManager) << "Token failed character validation" << token;
return false;
}
QString getTokenQueryString = QString("SELECT * FROM tokens WHERE token = \"%1\";")
.arg(QString::fromUtf8(token));
QSqlQuery getTokenQuery(m_db);
if (!getTokenQuery.exec(getTokenQueryString)) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
getTokenQuery.prepare("SELECT * FROM tokens WHERE token = :token;");
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;
}
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;
}
if (!getTokenQuery.first()) {
qCDebug(dcUserManager) << "Authorization failed for token" << token;
return false;
}
//qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
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()
{
m_db.close();
@ -549,26 +680,32 @@ bool UserManager::initDB()
}
int currentVersion = -1;
int newVersion = 1;
int newVersion = 2;
if (m_db.tables().contains("metadata")) {
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();
} else if (query.next()) {
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")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata...";
qCDebug(dcUserManager()) << "No \"users\" table found. Creating the table...";
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).");
m_db.close();
return false;
}
} else {
if (currentVersion < 1) {
qCDebug(dcUserManager()) << "Start user table database migration to version 1";
QSqlQuery query = QSqlQuery(m_db);
if (!query.exec("ALTER TABLE users ADD COLUMN scopes TEXT;") || m_db.lastError().isValid()) {
dumpDBError("Error migrating user database (table users).");
@ -608,86 +745,118 @@ bool UserManager::initDB()
m_db.close();
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")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata...";
qCDebug(dcUserManager()) << "No \"tokens\" table found. Creating the table...";
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()) {
dumpDBError("Error initializing user database (table tokens).");
dumpDBError("Error initializing user database (table tokens)");
m_db.close();
return false;
}
}
if (m_db.tables().contains("metadata")) {
if (currentVersion < newVersion) {
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 {
if (!m_db.tables().contains("metadata")) {
qCDebug(dcUserManager()) << "No \"metadata\" table found. Creating the table...";
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)!");
m_db.close();
return false;
}
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)!");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Successfully initialized user database.";
}
// 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";
} else {
// All migrations have been done
if (currentVersion < newVersion) {
QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) values(?, ?, ?, ?, ?, ?);");
query.addBindValue("");
query.addBindValue("");
query.addBindValue("Admin");
query.addBindValue("");
query.addBindValue("");
query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin));
query.exec();
if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText();
query.prepare("UPDATE metadata SET data = :version WHERE key = 'version'");
query.bindValue(":version", newVersion);
if (!query.exec() || m_db.lastError().isValid()) {
dumpDBError("Error updating database version");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Finished database migration to version" << newVersion;
}
}
qCDebug(dcUserManager()) << "User database initialized successfully.";
qCDebug(dcUserManager()) << "User database initialized successfully";
return true;
}
void UserManager::rotate(const QString &dbName)
{
int index = 1;
while (QFileInfo(QString("%1.%2").arg(dbName).arg(index)).exists()) {
while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index)))
index++;
}
qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index);
QFile f(dbName);
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
{
QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}");
static QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}");
return validator.match(username).hasMatch();
}
bool UserManager::validatePassword(const QString &password) const
{
if (password.length() < 8) {
if (password.length() < 8)
return false;
}
if (!password.contains(QRegularExpression("[a-z]"))) {
static QRegularExpression lowerRe("[a-z]");
if (!password.contains(lowerRe))
return false;
}
if (!password.contains(QRegularExpression("[A-Z]"))) {
static QRegularExpression upperRe("[A-Z]");
if (!password.contains(upperRe))
return false;
}
if (!password.contains(QRegularExpression("[0-9]"))) {
static QRegularExpression numbersRe("[0-9]");
if (!password.contains(numbersRe))
return false;
}
return true;
}
bool UserManager::validateToken(const QByteArray &token) const
{
QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)"));
static QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)"));
return validator.match(token).hasMatch();
}
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)
{
qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
}
void UserManager::evaluateAllowedThingsForUser()
{
}
void UserManager::onPushButtonPressed()
{
if (m_pushButtonTransaction.first == -1) {
@ -758,11 +976,11 @@ void UserManager::onPushButtonPressed()
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("")
.arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(m_pushButtonTransaction.second);
.arg(QUuid::createUuid().toString())
.arg("")
.arg(QString::fromUtf8(token))
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(m_pushButtonTransaction.second);
QSqlQuery storeTokenQuery(m_db);
if (!storeTokenQuery.exec(storeTokenQueryString) || m_db.lastError().type() != QSqlError::NoError) {

View File

@ -46,7 +46,8 @@ public:
UserErrorDuplicateUserId,
UserErrorBadPassword,
UserErrorTokenNotFound,
UserErrorPermissionDenied
UserErrorPermissionDenied,
UserErrorInconsistantScopes
};
Q_ENUM(UserError)
@ -55,10 +56,10 @@ public:
bool initRequired() 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 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);
bool pushButtonAuthAvailable() const;
@ -77,21 +78,33 @@ public:
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:
void userAdded(const QString &username);
void userRemoved(const QString &username);
void userChanged(const QString &username);
void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
void userThingRestrictionsChanged(const nymeaserver::UserInfo &userInfo, const ThingId &thingId, bool accessGranted);
private:
bool initDB();
void rotate(const QString &dbName);
bool validateUsername(const QString &username) const;
bool validatePassword(const QString &password) const;
bool validateToken(const QByteArray &token) const;
bool validateScopes(Types::PermissionScopes scopes) const;
void dumpDBError(const QString &message);
void evaluateAllowedThingsForUser();
private slots:
void onPushButtonPressed();
@ -102,7 +115,9 @@ private:
QPair<int, QString> m_pushButtonTransaction;
};
}
Q_DECLARE_METATYPE(nymeaserver::UserManager::UserError)
#endif // USERMANAGER_H

View File

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

View File

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

View File

@ -44,6 +44,27 @@ QString Types::scopeToString(Types::PermissionScope 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)
{
QMetaEnum metaEnum = QMetaEnum::fromType<PermissionScope>();

View File

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

View File

@ -11,7 +11,7 @@ isEmpty(NYMEA_VERSION) {
# define protocol versions
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}"
LIBNYMEA_API_VERSION_MAJOR=9
LIBNYMEA_API_VERSION_MINOR=0

View File

@ -1,4 +1,4 @@
8.3
8.4
{
"enums": {
"BasicType": [
@ -157,6 +157,7 @@
"PermissionScopeNone",
"PermissionScopeControlThings",
"PermissionScopeConfigureThings",
"PermissionScopeAccessAllThings",
"PermissionScopeExecuteRules",
"PermissionScopeConfigureRules",
"PermissionScopeAdmin"
@ -365,7 +366,8 @@
"UserErrorDuplicateUserId",
"UserErrorBadPassword",
"UserErrorTokenNotFound",
"UserErrorPermissionDenied"
"UserErrorPermissionDenied",
"UserErrorInconsistantScopes"
],
"ValueOperator": [
"ValueOperatorEquals",
@ -897,10 +899,10 @@
},
"permissionScope": "PermissionScopeNone",
"returns": {
"items": [
"o:displayMessage": "String",
"o:items": [
"$ref:BrowserItem"
],
"o:displayMessage": "String",
"thingError": "$ref:ThingError"
}
},
@ -1046,7 +1048,8 @@
},
"permissionScope": "PermissionScopeNone",
"returns": {
"ioConnections": "$ref:IOConnections"
"o:ioConnections": "$ref:IOConnections",
"thingError": "$ref:ThingError"
}
},
"Integrations.GetPluginConfiguration": {
@ -1969,9 +1972,23 @@
"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 no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user.",
"Users.ChangeUserPassword": {
"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": {
"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:email": "String",
"o:scopes": "$ref:PermissionScopes",
@ -2003,6 +2020,17 @@
"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": {
"description": "Return a list of all users in the system.",
"params": {
@ -2013,7 +2041,7 @@
}
},
"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": {
"tokenId": "Uuid"
},
@ -2045,8 +2073,11 @@
}
},
"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": {
"o:allowedThingIds": [
"Uuid"
],
"scopes": "$ref:PermissionScopes",
"username": "String"
},
@ -3249,6 +3280,9 @@
"sslEnabled": "Bool"
},
"UserInfo": {
"r:allowedThingIds": [
"Uuid"
],
"r:displayName": "String",
"r:email": "String",
"r:scopes": "$ref:PermissionScopes",

View File

@ -22,9 +22,7 @@
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <QtTest>
#include "logging/logengine.h"
#include "testusermanager.h"
#include "nymeacore.h"
#include "nymeatestbase.h"
#include "usermanager/usermanager.h"
@ -33,80 +31,10 @@
#include "../../utils/pushbuttonagent.h"
#include "../plugins/mock/extern-plugininfo.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();
private:
// m_apiToken is in testBase
QUuid m_tokenId;
};
TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
{
QCoreApplication::instance()->setOrganizationName("nymea-test");
@ -116,11 +44,11 @@ void TestUsermanager::initTestCase()
{
NymeaDBusService::setBusType(QDBusConnection::SessionBus);
NymeaTestBase::initTestCase("*.debug=false\n"
"Application.debug=true\n"
"Tests.debug=true\n"
"UserManager.debug=true\n"
"PushButtonAgent.debug=true\n"
"MockDevice.debug=true");
"Application.debug=true\n"
"Tests.debug=true\n"
"UserManager.debug=true\n"
"PushButtonAgent.debug=true\n"
"MockDevice.debug=true");
}
void TestUsermanager::init()
@ -357,7 +285,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
// Create a new clientId for alice and connect it to the server
QUuid aliceId = QUuid::createUuid();
m_mockTcpServer->clientConnected(aliceId);
emit m_mockTcpServer->clientConnected(aliceId);
m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait();
@ -368,12 +296,12 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
// Disconnect alice
m_mockTcpServer->clientDisconnected(aliceId);
emit m_mockTcpServer->clientDisconnected(aliceId);
// Now try with bob
// Create a new clientId for bob and connect it to the server
QUuid bobId = QUuid::createUuid();
m_mockTcpServer->clientConnected(bobId);
emit m_mockTcpServer->clientConnected(bobId);
clientSpy.clear();
m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait();
@ -495,9 +423,8 @@ void TestUsermanager::authenticateAfterPasswordChangeFail()
QVERIFY2(disconnectedSpy.count() == 1, "Connection should have dropped");
QTest::qWait(3200);
m_mockTcpServer->clientConnected(m_clientId);
emit m_mockTcpServer->clientConnected(m_clientId);
injectAndWait("JSONRPC.Hello");
}
void TestUsermanager::getUserInfo()
@ -505,14 +432,9 @@ void TestUsermanager::getUserInfo()
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"));
}
void TestUsermanager::unauthenticatedCallAfterTokenRemove()
@ -530,9 +452,270 @@ void TestUsermanager::unauthenticatedCallAfterTokenRemove()
QVERIFY2(spy.count() == 1, "Connection should be terminated!");
QTest::qWait(3200);
m_mockTcpServer->clientConnected(m_clientId);
emit m_mockTcpServer->clientConnected(m_clientId);
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)

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
HEADERS += ../../utils/pushbuttonagent.h
HEADERS += ../../utils/pushbuttonagent.h \
testusermanager.h
SOURCES += testusermanager.cpp \
../../utils/pushbuttonagent.cpp