Add initial test for thing based authentication
This commit is contained in:
parent
71cd3561b6
commit
f77d94ef7b
@ -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 ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetVendors(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
QVariantList vendors;
|
||||
@ -544,7 +544,7 @@ JsonReply* IntegrationsHandler::GetVendors(const QVariantMap ¶ms, const Json
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetThingClasses(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetThingClasses(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
{
|
||||
QVariantMap returns;
|
||||
QVariantList thingClasses;
|
||||
@ -603,13 +603,13 @@ JsonReply *IntegrationsHandler::DiscoverThings(const QVariantMap ¶ms, const
|
||||
}
|
||||
|
||||
reply->setData(returns);
|
||||
reply->finished();
|
||||
emit reply->finished();
|
||||
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetPlugins(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetPlugins(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
{
|
||||
Q_UNUSED(params)
|
||||
QVariantList plugins;
|
||||
@ -643,7 +643,7 @@ JsonReply *IntegrationsHandler::GetPluginConfiguration(const QVariantMap ¶ms
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap ¶ms)
|
||||
JsonReply *IntegrationsHandler::SetPluginConfiguration(const QVariantMap ¶ms)
|
||||
{
|
||||
QVariantMap returns;
|
||||
PluginId pluginId = PluginId(params.value("pluginId").toString());
|
||||
@ -653,7 +653,7 @@ JsonReply* IntegrationsHandler::SetPluginConfiguration(const QVariantMap ¶ms
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::AddThing(const QVariantMap ¶ms, const JsonContext &context)
|
||||
JsonReply *IntegrationsHandler::AddThing(const QVariantMap ¶ms, const JsonContext &context)
|
||||
{
|
||||
ThingClassId thingClassId(params.value("thingClassId").toString());
|
||||
QString thingName = params.value("name").toString();
|
||||
@ -670,7 +670,7 @@ JsonReply* IntegrationsHandler::AddThing(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, const JsonC
|
||||
}
|
||||
|
||||
jsonReply->setData(returns);
|
||||
jsonReply->finished();
|
||||
emit jsonReply->finished();
|
||||
});
|
||||
|
||||
return jsonReply;
|
||||
@ -762,20 +762,20 @@ JsonReply *IntegrationsHandler::ConfirmPairing(const QVariantMap ¶ms)
|
||||
returns.insert("thingId", info->thingId().toString());
|
||||
}
|
||||
jsonReply->setData(returns);
|
||||
jsonReply->finished();
|
||||
emit jsonReply->finished();
|
||||
});
|
||||
|
||||
return jsonReply;
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetThings(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetThings(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, 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 ¶ms)
|
||||
return createReply(statusToReply(status));
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::RemoveThing(const QVariantMap ¶ms)
|
||||
JsonReply *IntegrationsHandler::RemoveThing(const QVariantMap ¶ms)
|
||||
{
|
||||
QVariantMap returns;
|
||||
ThingId thingId = ThingId(params.value("thingId").toString());
|
||||
@ -937,7 +934,7 @@ JsonReply *IntegrationsHandler::SetStateFilter(const QVariantMap ¶ms)
|
||||
return createReply(statusToReply(status));
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetEventTypes(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetEventTypes(const QVariantMap ¶ms, 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 ¶ms, const J
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetActionTypes(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetActionTypes(const QVariantMap ¶ms, 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 ¶ms, const
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetStateTypes(const QVariantMap ¶ms, const JsonContext &context) const
|
||||
JsonReply *IntegrationsHandler::GetStateTypes(const QVariantMap ¶ms, 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 ¶ms, const J
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* IntegrationsHandler::GetStateValue(const QVariantMap ¶ms) const
|
||||
JsonReply *IntegrationsHandler::GetStateValue(const QVariantMap ¶ms, 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 ¶ms) const
|
||||
JsonReply *IntegrationsHandler::GetStateValues(const QVariantMap ¶ms, 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 ¶ms) const
|
||||
|
||||
JsonReply *IntegrationsHandler::BrowseThing(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, const Jso
|
||||
|
||||
JsonReply *IntegrationsHandler::GetBrowserItem(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, const
|
||||
JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, const J
|
||||
JsonReply *IntegrationsHandler::ExecuteBrowserItem(const QVariantMap ¶ms, 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 ¶ms, 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 ¶ms, co
|
||||
JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap ¶ms, 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 ¶
|
||||
data.insert("displayMessage", info->translatedDisplayMessage(context.locale()));
|
||||
}
|
||||
jsonReply->setData(data);
|
||||
jsonReply->finished();
|
||||
emit jsonReply->finished();
|
||||
});
|
||||
|
||||
return jsonReply;
|
||||
}
|
||||
|
||||
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap ¶ms)
|
||||
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap ¶ms, 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);
|
||||
|
||||
@ -61,8 +61,9 @@ public:
|
||||
Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
Q_INVOKABLE JsonReply *GetStateValue(const QVariantMap ¶ms) const;
|
||||
Q_INVOKABLE JsonReply *GetStateValues(const QVariantMap ¶ms) const;
|
||||
|
||||
Q_INVOKABLE JsonReply *GetStateValue(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
Q_INVOKABLE JsonReply *GetStateValues(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
|
||||
Q_INVOKABLE JsonReply *BrowseThing(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap ¶ms, const JsonContext &context) const;
|
||||
@ -71,7 +72,7 @@ public:
|
||||
Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap ¶ms, const JsonContext &context);
|
||||
Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap ¶ms, const JsonContext &context);
|
||||
|
||||
Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap ¶ms, const JsonContext &context);
|
||||
Q_INVOKABLE JsonReply *ConnectIO(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply *DisconnectIO(const QVariantMap ¶ms);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
125
tests/auto/usermanager/testusermanager.h
Normal file
125
tests/auto/usermanager/testusermanager.h
Normal 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
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user