Add initial test for thing based authentication

This commit is contained in:
Simon Stürz 2025-10-24 16:25:59 +02:00
parent 71cd3561b6
commit f77d94ef7b
9 changed files with 435 additions and 197 deletions

View File

@ -131,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));
@ -148,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));
@ -180,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));
@ -200,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>());
@ -314,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>());
@ -530,7 +530,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;
@ -544,7 +544,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;
@ -603,13 +603,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;
@ -643,7 +643,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());
@ -653,7 +653,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();
@ -670,7 +670,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);
@ -690,7 +690,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;
@ -735,7 +735,7 @@ JsonReply *IntegrationsHandler::PairThing(const QVariantMap &params, const JsonC
}
jsonReply->setData(returns);
jsonReply->finished();
emit jsonReply->finished();
});
return jsonReply;
@ -762,20 +762,20 @@ 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 (NymeaCore::instance()->userManager()->restrictedThingAccess(context.token())) {
QList<ThingId> allowedThingIds = NymeaCore::instance()->userManager()->allowedThingIds(context.token());
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);
@ -795,7 +795,6 @@ JsonReply* IntegrationsHandler::GetThings(const QVariantMap &params, const JsonC
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()) {
@ -858,13 +857,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;
@ -882,7 +879,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());
@ -937,7 +934,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());
@ -947,7 +944,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());
@ -957,7 +954,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());
@ -967,28 +964,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()));
@ -997,7 +1000,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");
@ -1007,15 +1013,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;
@ -1023,10 +1030,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());
@ -1039,7 +1047,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;
@ -1048,6 +1056,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();
@ -1065,7 +1076,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;
@ -1074,6 +1085,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);
@ -1087,7 +1101,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;
@ -1096,6 +1110,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"));
@ -1111,15 +1128,18 @@ 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);

View File

@ -61,8 +61,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 +72,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);

View File

@ -656,7 +656,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;
}

View File

@ -195,14 +195,16 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds) 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.addBindValue(Types::thingIdsToStringList(thingIds).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();
@ -241,14 +243,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;
}
@ -263,7 +266,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();
@ -291,25 +294,27 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types
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;
foreach (const ThingId &thingId, allowedThingIds) {
if (NymeaCore::instance()->thingManager()->configuredThings().findById(thingId) == nullptr) {
qCWarning(dcUserManager()) << "Cannot set user scope for" << username << "because there is no thing with ID ";
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);
}
}
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 = :scopes, allowedThingIds = :allowedThingIds WHERE username = :username");
setScopesQuery.bindValue(":username", username);
setScopesQuery.bindValue(":scopes", scopesString);
setScopesQuery.bindValue(":allowedThingIds", allowedThingIdsString);
setScopesQuery.bindValue(":username", username);
if (!setScopesQuery.exec()) {
qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText();
return UserErrorBackendError;
@ -575,16 +580,23 @@ bool UserManager::verifyToken(const QByteArray &token)
return true;
}
bool UserManager::restrictedThingAccess(const QByteArray &token) const
bool UserManager::hasRestrictedThingAccess(const QByteArray &token) const
{
UserInfo ui = userInfo(tokenInfo(token).username());
return !ui.scopes().testFlag(Types::PermissionScopeAccessAllThings);
}
QList<ThingId> UserManager::allowedThingIds(const QByteArray &token) const
bool UserManager::accessToThingGranted(const ThingId &thingId, const QByteArray &token)
{
UserInfo ui = userInfo(tokenInfo(token).username());
return ui.allowedThingIds();
if (!hasRestrictedThingAccess(token))
return true;
return getAllowedThingIdsForToken(token).contains(thingId);
}
QList<ThingId> UserManager::getAllowedThingIdsForToken(const QByteArray &token) const
{
return userInfo(tokenInfo(token).username()).allowedThingIds();
}
bool UserManager::initDB()

View File

@ -77,8 +77,9 @@ public:
bool verifyToken(const QByteArray &token);
bool restrictedThingAccess(const QByteArray &token) const;
QList<ThingId> allowedThingIds(const QByteArray &token) const;
bool hasRestrictedThingAccess(const QByteArray &token) const;
bool accessToThingGranted(const ThingId &thingId, const QByteArray &token);
QList<ThingId> getAllowedThingIdsForToken(const QByteArray &token) const;
signals:
void userAdded(const QString &username);

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

@ -22,9 +22,7 @@
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include <QtTest>
#include "logging/logengine.h"
#include "testusermanager.h"
#include "nymeacore.h"
#include "nymeatestbase.h"
#include "usermanager/usermanager.h"
@ -33,83 +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();
void testScopeConsitancy_data();
void testScopeConsitancy();
private:
// m_apiToken is in testBase
QUuid m_tokenId;
};
TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
{
QCoreApplication::instance()->setOrganizationName("nymea-test");
@ -639,5 +564,159 @@ void TestUsermanager::testScopeConsitancy()
QCOMPARE(response.toMap().value("params").toMap().value("error").toString(), error);
}
#include "testusermanager.moc"
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;
httpportParamOne.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
httpportParamOne.insert("value", m_mockThing1Port - 2);
thingParams.clear();
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));
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
params.clear();
params.insert("thingId", thingIdOne);
params.insert("stateTypeId", mockConnectedStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// BrowseThing
params.clear();
params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.BrowseThing", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// GetBrowserItem
params.clear();
params.insert("thingId", thingIdOne);
response = injectAndWait("Integrations.GetBrowserItem", params);
verifyError(response, "thingError", enumValueName(Thing::ThingErrorThingNotFound));
// Make sure notification get received from allowed thing
// Make sure no notification will be recived from restricted thing
// Clean up
}
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