Move authentication to Users namespace

This commit is contained in:
Michael Zanetti 2020-02-08 01:30:11 +01:00
parent ec0aa802c5
commit fb94178920
19 changed files with 640 additions and 144 deletions

View File

@ -109,12 +109,12 @@ QString ActionHandler::name() const
return "Actions";
}
JsonReply* ActionHandler::ExecuteAction(const QVariantMap &params)
JsonReply* ActionHandler::ExecuteAction(const QVariantMap &params, const JsonContext &context)
{
DeviceId deviceId(params.value("deviceId").toString());
ActionTypeId actionTypeId(params.value("actionTypeId").toString());
ParamList actionParams = unpack<ParamList>(params.value("params"));
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
Action action(actionTypeId, deviceId);
action.setParams(actionParams);
@ -135,9 +135,9 @@ JsonReply* ActionHandler::ExecuteAction(const QVariantMap &params)
return jsonReply;
}
JsonReply *ActionHandler::GetActionType(const QVariantMap &params) const
JsonReply *ActionHandler::GetActionType(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
qCDebug(dcJsonRpc) << "asked for action type" << params;
ActionTypeId actionTypeId(params.value("actionTypeId").toString());
foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) {

View File

@ -44,8 +44,8 @@ public:
QString name() const;
Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetActionType(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetActionType(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap &params);
Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap &params);

View File

@ -415,13 +415,12 @@ QString DeviceHandler::name() const
return "Devices";
}
JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap &params) const
JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
Q_UNUSED(params)
QVariantList vendors;
foreach (const Vendor &vendor, NymeaCore::instance()->deviceManager()->supportedVendors()) {
Vendor translatedVendor = NymeaCore::instance()->deviceManager()->translateVendor(vendor, locale);
Vendor translatedVendor = NymeaCore::instance()->deviceManager()->translateVendor(vendor, context.locale());
vendors.append(pack(translatedVendor));
}
@ -430,14 +429,13 @@ JsonReply* DeviceHandler::GetSupportedVendors(const QVariantMap &params) const
return createReply(returns);
}
JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap &params) const
JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
VendorId vendorId = VendorId(params.value("vendorId").toString());
QVariantMap returns;
QVariantList deviceClasses;
foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices(vendorId)) {
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale);
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale());
deviceClasses.append(pack(translatedDeviceClass));
}
@ -445,9 +443,9 @@ JsonReply* DeviceHandler::GetSupportedDevices(const QVariantMap &params) const
return createReply(returns);
}
JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap &params) const
JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
QVariantMap returns;
@ -479,14 +477,13 @@ JsonReply *DeviceHandler::GetDiscoveredDevices(const QVariantMap &params) const
return reply;
}
JsonReply* DeviceHandler::GetPlugins(const QVariantMap &params) const
JsonReply* DeviceHandler::GetPlugins(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
Q_UNUSED(params)
QVariantList plugins;
foreach (DevicePlugin* plugin, NymeaCore::instance()->deviceManager()->plugins()) {
QVariantMap packedPlugin = pack(*plugin).toMap();
packedPlugin["displayName"] = NymeaCore::instance()->deviceManager()->translate(plugin->pluginId(), plugin->pluginDisplayName(), locale);
packedPlugin["displayName"] = NymeaCore::instance()->deviceManager()->translate(plugin->pluginId(), plugin->pluginDisplayName(), context.locale());
plugins.append(packedPlugin);
}
@ -524,13 +521,13 @@ JsonReply* DeviceHandler::SetPluginConfiguration(const QVariantMap &params)
return createReply(returns);
}
JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap &params)
JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap &params, const JsonContext &context)
{
DeviceClassId deviceClassId(params.value("deviceClassId").toString());
QString deviceName = params.value("name").toString();
ParamList deviceParams = unpack<ParamList>(params.value("deviceParams"));
DeviceDescriptorId deviceDescriptorId(params.value("deviceDescriptorId").toString());
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
JsonReply *jsonReply = createAsyncReply("AddConfiguredDevice");
@ -558,11 +555,11 @@ JsonReply* DeviceHandler::AddConfiguredDevice(const QVariantMap &params)
return jsonReply;
}
JsonReply *DeviceHandler::PairDevice(const QVariantMap &params)
JsonReply *DeviceHandler::PairDevice(const QVariantMap &params, const JsonContext &context)
{
QString deviceName = params.value("name").toString();
ParamList deviceParams = unpack<ParamList>(params.value("deviceParams"));
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
DevicePairingInfo *info;
if (params.contains("deviceDescriptorId")) {
@ -603,12 +600,12 @@ JsonReply *DeviceHandler::PairDevice(const QVariantMap &params)
return jsonReply;
}
JsonReply *DeviceHandler::ConfirmPairing(const QVariantMap &params)
JsonReply *DeviceHandler::ConfirmPairing(const QVariantMap &params, const JsonContext &context)
{
PairingTransactionId pairingTransactionId = PairingTransactionId(params.value("pairingTransactionId").toString());
QString secret = params.value("secret").toString();
QString username = params.value("username").toString();
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
JsonReply *jsonReply = createAsyncReply("ConfirmPairing");
@ -651,12 +648,12 @@ JsonReply* DeviceHandler::GetConfiguredDevices(const QVariantMap &params) const
return createReply(returns);
}
JsonReply *DeviceHandler::ReconfigureDevice(const QVariantMap &params)
JsonReply *DeviceHandler::ReconfigureDevice(const QVariantMap &params, const JsonContext &context)
{
DeviceId deviceId = DeviceId(params.value("deviceId").toString());
ParamList deviceParams = unpack<ParamList>(params.value("deviceParams"));
DeviceDescriptorId deviceDescriptorId(params.value("deviceDescriptorId").toString());
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
JsonReply *jsonReply = createAsyncReply("ReconfigureDevice");
@ -739,36 +736,30 @@ JsonReply *DeviceHandler::SetDeviceSettings(const QVariantMap &params)
return createReply(statusToReply(status));
}
JsonReply* DeviceHandler::GetEventTypes(const QVariantMap &params) const
JsonReply* DeviceHandler::GetEventTypes(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString()));
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale);
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale());
QVariantMap returns;
returns.insert("eventTypes", pack(translatedDeviceClass.eventTypes()));
return createReply(returns);
}
JsonReply* DeviceHandler::GetActionTypes(const QVariantMap &params) const
JsonReply* DeviceHandler::GetActionTypes(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString()));
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale);
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale());
QVariantMap returns;
returns.insert("actionTypes", pack(translatedDeviceClass.actionTypes()));
return createReply(returns);
}
JsonReply* DeviceHandler::GetStateTypes(const QVariantMap &params) const
JsonReply* DeviceHandler::GetStateTypes(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
DeviceClass deviceClass = NymeaCore::instance()->deviceManager()->findDeviceClass(DeviceClassId(params.value("deviceClassId").toString()));
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, locale);
DeviceClass translatedDeviceClass = NymeaCore::instance()->deviceManager()->translateDeviceClass(deviceClass, context.locale());
QVariantMap returns;
returns.insert("stateTypes", pack(translatedDeviceClass.stateTypes()));
@ -803,14 +794,14 @@ JsonReply *DeviceHandler::GetStateValues(const QVariantMap &params) const
return createReply(returns);
}
JsonReply *DeviceHandler::BrowseDevice(const QVariantMap &params) const
JsonReply *DeviceHandler::BrowseDevice(const QVariantMap &params, const JsonContext &context) const
{
DeviceId deviceId = DeviceId(params.value("deviceId").toString());
QString itemId = params.value("itemId").toString();
JsonReply *jsonReply = createAsyncReply("BrowseDevice");
BrowseResult *result = NymeaCore::instance()->deviceManager()->browseDevice(deviceId, itemId, params.value("locale").toLocale());
BrowseResult *result = NymeaCore::instance()->deviceManager()->browseDevice(deviceId, itemId, context.locale());
connect(result, &BrowseResult::finished, jsonReply, [this, jsonReply, result](){
QVariantMap returns = statusToReply(result->status());
@ -826,7 +817,7 @@ JsonReply *DeviceHandler::BrowseDevice(const QVariantMap &params) const
return jsonReply;
}
JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap &params) const
JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap &params, const JsonContext &context) const
{
QVariantMap returns;
DeviceId deviceId = DeviceId(params.value("deviceId").toString());
@ -834,7 +825,7 @@ JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap &params) const
JsonReply *jsonReply = createAsyncReply("GetBrowserItem");
BrowserItemResult *result = NymeaCore::instance()->deviceManager()->browserItemDetails(deviceId, itemId, params.value("locale").toLocale());
BrowserItemResult *result = NymeaCore::instance()->deviceManager()->browserItemDetails(deviceId, itemId, context.locale());
connect(result, &BrowserItemResult::finished, jsonReply, [this, jsonReply, result](){
QVariantMap params = statusToReply(result->status());
if (result->status() == Device::DeviceErrorNoError) {
@ -847,12 +838,12 @@ JsonReply *DeviceHandler::GetBrowserItem(const QVariantMap &params) const
return jsonReply;
}
JsonReply *DeviceHandler::ExecuteAction(const QVariantMap &params)
JsonReply *DeviceHandler::ExecuteAction(const QVariantMap &params, const JsonContext &context)
{
DeviceId deviceId(params.value("deviceId").toString());
ActionTypeId actionTypeId(params.value("actionTypeId").toString());
ParamList actionParams = unpack<ParamList>(params.value("params"));
QLocale locale = params.value("locale").toLocale();
QLocale locale = context.locale();
Action action(actionTypeId, deviceId);
action.setParams(actionParams);

View File

@ -44,32 +44,32 @@ public:
QString name() const override;
Q_INVOKABLE JsonReply *GetSupportedVendors(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetSupportedDevices(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetDiscoveredDevices(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetPlugins(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetSupportedVendors(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetSupportedDevices(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetDiscoveredDevices(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetPlugins(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetPluginConfiguration(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *SetPluginConfiguration(const QVariantMap &params);
Q_INVOKABLE JsonReply *AddConfiguredDevice(const QVariantMap &params);
Q_INVOKABLE JsonReply *PairDevice(const QVariantMap &params);
Q_INVOKABLE JsonReply *ConfirmPairing(const QVariantMap &params);
Q_INVOKABLE JsonReply *AddConfiguredDevice(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *PairDevice(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ConfirmPairing(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetConfiguredDevices(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *ReconfigureDevice(const QVariantMap &params);
Q_INVOKABLE JsonReply *ReconfigureDevice(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *EditDevice(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveConfiguredDevice(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetDeviceSettings(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetStateTypes(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *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 *BrowseDevice(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *BrowseDevice(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetBrowserItem(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap &params);
Q_INVOKABLE JsonReply *ExecuteAction(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap &params);
Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap &params);

View File

@ -93,16 +93,15 @@ void EventHandler::eventTriggered(const Event &event)
emit EventTriggered(params);
}
JsonReply* EventHandler::GetEventType(const QVariantMap &params) const
JsonReply* EventHandler::GetEventType(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
qCDebug(dcJsonRpc) << "asked for event type" << params;
EventTypeId eventTypeId(params.value("eventTypeId").toString());
foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) {
foreach (const EventType &eventType, deviceClass.eventTypes()) {
if (eventType.id() == eventTypeId) {
EventType translatedEventType = eventType;
translatedEventType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), eventType.displayName(), locale));
translatedEventType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), eventType.displayName(), context.locale()));
QVariantMap data;
data.insert("deviceError", enumValueName<Device::DeviceError>(Device::DeviceErrorNoError));
data.insert("eventType", pack(translatedEventType));

View File

@ -44,7 +44,7 @@ public:
explicit EventHandler(QObject *parent = nullptr);
QString name() const override;
Q_INVOKABLE JsonReply *GetEventType(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetEventType(const QVariantMap &params, const JsonContext &context) const;
signals:
void EventTriggered(const QVariantMap &params);

View File

@ -159,7 +159,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.insert("username", enumValueName(String));
params.insert("password", enumValueName(String));
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("CreateUser", description, params, returns);
registerMethod("CreateUser", description, params, returns, "Use Users.CreateUser instead.");
params.clear(); returns.clear();
description = "Authenticate a client to the api via user & password challenge. Provide "
@ -171,7 +171,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.insert("deviceName", enumValueName(String));
returns.insert("success", enumValueName(Bool));
returns.insert("o:token", enumValueName(String));
registerMethod("Authenticate", description, params, returns);
registerMethod("Authenticate", description, params, returns, "Use Users.Authenticate instead.");
params.clear(); returns.clear();
description = "Authenticate a client to the api via Push Button method. "
@ -190,7 +190,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.insert("deviceName", enumValueName(String));
returns.insert("success", enumValueName(Bool));
returns.insert("transactionId", enumValueName(Int));
registerMethod("RequestPushButtonAuth", description, params, returns);
registerMethod("RequestPushButtonAuth", description, params, returns, "Use Users.RequestPushButtonAuth instead.");
params.clear(); returns.clear();
description = "Return a list of TokenInfo objects of all the tokens for the current user.";
@ -246,7 +246,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration
params.insert("success", enumValueName(Bool));
params.insert("transactionId", enumValueName(Int));
params.insert("o:token", enumValueName(String));
registerNotification("PushButtonAuthFinished", description, params);
registerNotification("PushButtonAuthFinished", description, params, "Use Users.PushButtonAuthFinished instead.");
QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection);
@ -259,12 +259,12 @@ QString JsonRPCServerImplementation::name() const
return QStringLiteral("JSONRPC");
}
JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap &params)
JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap &params, const JsonContext &context)
{
TransportInterface *interface = reinterpret_cast<TransportInterface*>(property("transportInterface").toLongLong());
qCDebug(dcJsonRpc()) << params;
QUuid clientId = this->property("clientId").toUuid();
QUuid clientId = context.clientId();
if (params.contains("locale")) {
m_clientLocales.insert(clientId, QLocale(params.value("locale").toString()));
}
@ -297,9 +297,9 @@ JsonReply* JsonRPCServerImplementation::Version(const QVariantMap &params) const
return createReply(data);
}
JsonReply* JsonRPCServerImplementation::SetNotificationStatus(const QVariantMap &params)
JsonReply* JsonRPCServerImplementation::SetNotificationStatus(const QVariantMap &params, const JsonContext &context)
{
QUuid clientId = this->property("clientId").toUuid();
QUuid clientId = context.clientId();
Q_ASSERT_X(m_clientTransports.contains(clientId), "JsonRPCServer", "Invalid client ID.");
QStringList enabledNamespaces;
@ -351,10 +351,10 @@ JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap &params)
return createReply(ret);
}
JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap &params)
JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap &params, const JsonContext &context)
{
QString deviceName = params.value("deviceName").toString();
QUuid clientId = this->property("clientId").toUuid();
QUuid clientId = context.clientId();
int transactionId = NymeaCore::instance()->userManager()->requestPushButtonAuth(deviceName);
m_pushButtonTransactions.insert(transactionId, clientId);
@ -366,12 +366,11 @@ JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap
return createReply(data);
}
JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap &params) const
JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap &params, const JsonContext &context) const
{
Q_UNUSED(params)
QByteArray token = property("token").toByteArray();
TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token);
TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token());
QList<TokenInfo> tokens = NymeaCore::instance()->userManager()->tokens(tokenInfo.username());
QVariantList retList;
foreach (const TokenInfo &tokenInfo, tokens) {
@ -636,8 +635,8 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
// check if authentication is required for this transport
if (m_interfaces.value(interface)) {
QByteArray token = message.value("token").toByteArray();
QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.CreateUser"};
QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"};
QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.RequestPushButtonAuth", "Users.CreateUser"};
QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth", "Users.Authenticate", "Users.RequestPushButtonAuth"};
// if there is no user in the system yet, let's fail unless this is special method for authentication itself
if (NymeaCore::instance()->userManager()->initRequired()) {
if (!authExemptMethodsNoUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) {
@ -683,13 +682,6 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
return;
}
// Hack: attach some properties to the handler to be able to handle the JSONRPC methods. Do not use this outside of jsonrpcserver
handler->setProperty("clientId", clientId);
handler->setProperty("token", message.value("token").toByteArray());
handler->setProperty("transportInterface", reinterpret_cast<qint64>(interface));
qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace << method.toLatin1().data();
if (!(targetNamespace == "JSONRPC" && method == "Hello")) {
// This is not the handshake message. If we've waited for it, consider this a protocol violation and drop connection
if (m_newConnectionWaitTimers.contains(clientId)) {
@ -698,16 +690,25 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac
interface->terminateClientConnection(clientId);
return;
}
// Unless this is the Hello message, which allows setting the locale explicitly, attach the locale
// for this connection
// If the client did request a locale in the Hello message, use that locale
params.insert("locale", m_clientLocales.value(clientId));
}
// Attach the transportInterface if this call is for ourselves
if (handler == this) {
handler->setProperty("transportInterface", reinterpret_cast<qint64>(interface));
}
JsonContext callContext(clientId, m_clientLocales.value(clientId));
callContext.setToken(message.value("token").toByteArray());
qCDebug(dcJsonRpc()) << "Invoking method" << targetNamespace + '.' + method << "from client" << clientId;
JsonReply *reply;
QMetaObject::invokeMethod(handler, method.toLatin1().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params));
if (handler->metaObject()->indexOfMethod(method.toUtf8() + "(QVariantMap,JsonContext)") >= 0) {
QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params), Q_ARG(JsonContext, callContext));
} else {
QMetaObject::invokeMethod(handler, method.toUtf8().data(), Q_RETURN_ARG(JsonReply*, reply), Q_ARG(QVariantMap, params));
}
if (reply->type() == JsonReply::TypeAsync) {
m_asyncReplies.insert(reply, interface);
reply->setClientId(clientId);
@ -755,7 +756,7 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap &params)
JsonValidator validator;
Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(),
validator.result().where().toUtf8(),
validator.result().errorString().toUtf8());
validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
@ -773,6 +774,39 @@ void JsonRPCServerImplementation::sendNotification(const QVariantMap &params)
}
}
void JsonRPCServerImplementation::sendClientNotification(const QUuid &clientId, const QVariantMap &params)
{
JsonHandler *handler = qobject_cast<JsonHandler *>(sender());
QMetaMethod method = handler->metaObject()->method(senderSignalIndex());
if (!m_clientTransports.contains(clientId)) {
qCWarning(dcJsonRpc()) << "No client with id" << clientId << ". Not sending client notification.";
return;
}
QVariantMap notification;
notification.insert("id", m_notificationId++);
notification.insert("notification", handler->name() + "." + method.name());
notification.insert("params", params);
JsonValidator validator;
Q_ASSERT_X(validator.validateNotificationParams(params, handler->name() + '.' + method.name(), m_api).success(),
validator.result().where().toUtf8(),
validator.result().errorString().toUtf8() + "\nGot:" + QJsonDocument::fromVariant(params).toJson(QJsonDocument::Indented));
if (m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().contains("deprecated")) {
QString deprecationMessage = m_api.value("notifications").toMap().value(handler->name() + '.' + method.name()).toMap().value("deprecated").toString();
qCWarning(dcJsonRpc()) << "Client uses deprecated API. Please update client implementation!";
qCWarning(dcJsonRpc()) << handler->name() + '.' + method.name() + ':' << deprecationMessage;
notification.insert("deprecationWarning", deprecationMessage);
}
QByteArray data = QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact);
qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;
qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name();
m_clientTransports.value(clientId)->sendData(clientId, data);
}
void JsonRPCServerImplementation::asyncReplyFinished()
{
JsonReply *reply = qobject_cast<JsonReply *>(sender());
@ -834,12 +868,6 @@ void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bo
return;
}
TransportInterface *transport = m_clientTransports.value(clientId);
if (!transport) {
qCWarning(dcJsonRpc()) << "No transport for given clientId";
return;
}
QVariantMap params;
params.insert("transactionId", transactionId);
params.insert("success", success);
@ -847,12 +875,7 @@ void JsonRPCServerImplementation::onPushButtonAuthFinished(int transactionId, bo
params.insert("token", token);
}
QVariantMap notification;
notification.insert("id", transactionId);
notification.insert("notification", "JSONRPC.PushButtonAuthFinished");
notification.insert("params", params);
transport->sendData(clientId, QJsonDocument::fromVariant(notification).toJson(QJsonDocument::Compact));
emit PushButtonAuthFinished(clientId, params);
}
bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler)
@ -914,8 +937,10 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler)
QVariantMap newMethods;
foreach (const QString &methodName, handler->jsonMethods().keys()) {
QVariantMap method = handler->jsonMethods().value(methodName).toMap();
if (handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap)") < 0) {
qCWarning(dcJsonRpc()).nospace().noquote() << "Invalid method \"" << methodName << "\". Method \"JsonReply* " + methodName + "(QVariantMap)\" does not exist. Not registering handler " << handler->name();
if (handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap)") < 0
&& handler->metaObject()->indexOfMethod(methodName.toUtf8() + "(QVariantMap,JsonContext)") < 0) {
qCWarning(dcJsonRpc()).nospace().noquote() << "Invalid method \"" << methodName << "\". Method \"JsonReply* " + methodName + "(QVariantMap,JsonContext)\" does not exist. Not registering handler " << handler->name();
return false;
}
if (!JsonValidator::checkRefs(method.value("params").toMap(), apiIncludingThis)) {
@ -952,7 +977,11 @@ bool JsonRPCServerImplementation::registerHandler(JsonHandler *handler)
for (int i = 0; i < handler->metaObject()->methodCount(); ++i) {
QMetaMethod method = handler->metaObject()->method(i);
if (method.methodType() == QMetaMethod::Signal && QString(method.name()).contains(QRegExp("^[A-Z]"))) {
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)")));
if (method.parameterCount() == 1 && method.parameterType(0) == QVariant::Map) {
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendNotification(QVariantMap)")));
} else if (method.parameterCount() == 2 && method.parameterType(0) == QVariant::Uuid && method.parameterType(1) == QVariant::Map) {
QObject::connect(handler, method, this, metaObject()->method(metaObject()->indexOfSlot("sendClientNotification(QUuid,QVariantMap)")));
}
}
}
return true;

View File

@ -57,15 +57,15 @@ public:
// JsonHandler API implementation
QString name() const;
Q_INVOKABLE JsonReply *Hello(const QVariantMap &params);
Q_INVOKABLE JsonReply *Hello(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *Introspect(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *Version(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params);
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap &params);
Q_INVOKABLE JsonReply *Tokens(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *Tokens(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetupCloudConnection(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap &params);
@ -74,7 +74,7 @@ public:
signals:
void CloudConnectedChanged(const QVariantMap &map);
void PushButtonAuthFinished(const QVariantMap &params);
void PushButtonAuthFinished(const QUuid &clientId, const QVariantMap &params);
// Server API
public:
@ -103,6 +103,7 @@ private slots:
void processData(const QUuid &clientId, const QByteArray &data);
void sendNotification(const QVariantMap &params);
void sendClientNotification(const QUuid &clientId, const QVariantMap &params);
void asyncReplyFinished();

View File

@ -70,9 +70,8 @@ QString StateHandler::name() const
return "States";
}
JsonReply* StateHandler::GetStateType(const QVariantMap &params) const
JsonReply* StateHandler::GetStateType(const QVariantMap &params, const JsonContext &context) const
{
QLocale locale = params.value("locale").toLocale();
qCDebug(dcJsonRpc) << "asked for state type" << params;
StateTypeId stateTypeId(params.value("stateTypeId").toString());
foreach (const DeviceClass &deviceClass, NymeaCore::instance()->deviceManager()->supportedDevices()) {
@ -81,7 +80,7 @@ JsonReply* StateHandler::GetStateType(const QVariantMap &params) const
QVariantMap data;
data.insert("deviceError", enumValueName<Device::DeviceError>(Device::DeviceErrorNoError));
StateType translatedStateType = stateType;
translatedStateType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), stateType.displayName(), locale));
translatedStateType.setDisplayName(NymeaCore::instance()->deviceManager()->translate(deviceClass.pluginId(), stateType.displayName(), context.locale()));
data.insert("stateType", pack(translatedStateType));
return createReply(data);
}

View File

@ -42,7 +42,7 @@ public:
explicit StateHandler(QObject *parent = nullptr);
QString name() const override;
Q_INVOKABLE JsonReply *GetStateType(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *GetStateType(const QVariantMap &params, const JsonContext &context) const;
};

View File

@ -1,3 +1,33 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "usershandler.h"
#include "usermanager/usermanager.h"
#include "usermanager/userinfo.h"
@ -23,6 +53,37 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("CreateUser", description, params, returns);
params.clear(); returns.clear();
description = "Authenticate a client to the api via user & password challenge. Provide "
"a device name which allows the user to identify the client and revoke the token in case "
"the device is lost or stolen. This will return a new token to be used to authorize a "
"client at the API.";
params.insert("username", enumValueName(String));
params.insert("password", enumValueName(String));
params.insert("deviceName", enumValueName(String));
returns.insert("success", enumValueName(Bool));
returns.insert("o:token", enumValueName(String));
registerMethod("Authenticate", description, params, returns);
params.clear(); returns.clear();
description = "Authenticate a client to the api via Push Button method. "
"Provide a device name which allows the user to identify the client and revoke the "
"token in case the device is lost or stolen. If push button hardware is available, "
"this will return with success and start listening for push button presses. When the "
"push button is pressed, the PushButtonAuthFinished notification will be sent to the "
"requesting client. The procedure will be cancelled when the connection is interrupted. "
"If another client requests push button authentication while a procedure is still going "
"on, the second call will take over and the first one will be notified by the "
"PushButtonAuthFinished signal about the error. The application should make it clear "
"to the user to not press the button when the procedure fails as this can happen for 2 "
"reasons: a) a second user is trying to auth at the same time and only the currently "
"active user should press the button or b) it might indicate an attacker trying to take "
"over and snooping in for tokens.";
params.insert("deviceName", enumValueName(String));
returns.insert("success", enumValueName(Bool));
returns.insert("transactionId", enumValueName(Int));
registerMethod("RequestPushButtonAuth", description, params, returns);
params.clear(); returns.clear();
description = "Change the password for the currently logged in user.";
params.insert("newPassword", enumValueName(String));
@ -47,6 +108,18 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent):
returns.insert("error", enumRef<UserManager::UserError>());
registerMethod("RemoveToken", description, params, returns);
// Notifications
params.clear();
description = "Emitted when a push button authentication reaches final state. NOTE: This notification is "
"special. It will only be emitted to connections that did actively request a push button "
"authentication, but also it will be emitted regardless of the notification settings.";
params.insert("success", enumValueName(Bool));
params.insert("transactionId", enumValueName(Int));
params.insert("o:token", enumValueName(String));
registerNotification("PushButtonAuthFinished", description, params);
connect(m_userManager, &UserManager::pushButtonAuthFinished, this, &UsersHandler::onPushButtonAuthFinished);
}
QString UsersHandler::name() const
@ -66,11 +139,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
return createReply(returns);
}
JsonReply *UsersHandler::ChangePassword(const QVariantMap &params)
JsonReply *UsersHandler::ChangePassword(const QVariantMap &params, const JsonContext &context)
{
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot change password from an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
@ -106,13 +179,12 @@ JsonReply *UsersHandler::Authenticate(const QVariantMap &params)
return createReply(ret);
}
JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap &params)
JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap &params, const JsonContext &context)
{
QString deviceName = params.value("deviceName").toString();
QUuid clientId = this->property("clientId").toUuid();
int transactionId = m_userManager->requestPushButtonAuth(deviceName);
m_pushButtonTransactions.insert(transactionId, clientId);
m_pushButtonTransactions.insert(transactionId, context.clientId());
QVariantMap data;
data.insert("transactionId", transactionId);
@ -121,12 +193,12 @@ JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap &params)
return createReply(data);
}
JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params)
JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params, const JsonContext &context)
{
Q_UNUSED(params)
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot get user info form an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
@ -145,12 +217,12 @@ JsonReply *UsersHandler::GetUserInfo(const QVariantMap &params)
return createReply(ret);
}
JsonReply *UsersHandler::GetTokens(const QVariantMap &params)
JsonReply *UsersHandler::GetTokens(const QVariantMap &params, const JsonContext &context)
{
Q_UNUSED(params)
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot fetch tokens form an unauthenticated connection";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
@ -175,11 +247,11 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap &params)
return createReply(ret);
}
JsonReply *UsersHandler::RemoveToken(const QVariantMap &params)
JsonReply *UsersHandler::RemoveToken(const QVariantMap &params, const JsonContext &context)
{
QVariantMap ret;
QByteArray currentToken = property("token").toByteArray();
QByteArray currentToken = context.token();
if (currentToken.isEmpty()) {
qCWarning(dcJsonRpc()) << "Cannot remove a token from an unauthenticated connection.";
ret.insert("error", enumValueName<UserManager::UserError>(UserManager::UserErrorPermissionDenied));
@ -213,4 +285,24 @@ JsonReply *UsersHandler::RemoveToken(const QVariantMap &params)
return createReply(ret);
}
void UsersHandler::onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token)
{
Q_UNUSED(success)
Q_UNUSED(token)
QUuid clientId = m_pushButtonTransactions.take(transactionId);
if (clientId.isNull()) {
qCDebug(dcJsonRpc()) << "Received a PushButton reply but wasn't expecting it.";
return;
}
QVariantMap params;
params.insert("transactionId", transactionId);
params.insert("success", success);
if (success) {
params.insert("token", token);
}
emit PushButtonAuthFinished(clientId, params);
}
}

View File

@ -1,3 +1,33 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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 USERSHANDLER_H
#define USERSHANDLER_H
@ -18,15 +48,18 @@ public:
QString name() const override;
Q_INVOKABLE JsonReply *CreateUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap &params);
Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *Authenticate(const QVariantMap &params);
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetTokens(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params);
Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetTokens(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap &params, const JsonContext &context);
signals:
void PushButtonAuthFinished(const QVariantMap &params);
void PushButtonAuthFinished(const QUuid &clientId, const QVariantMap &params);
private slots:
void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
private:
UserManager *m_userManager = nullptr;

View File

@ -0,0 +1,58 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "jsoncontext.h"
JsonContext::JsonContext(const QUuid &clientId, const QLocale &locale):
m_clientId(clientId),
m_locale(locale)
{
}
QUuid JsonContext::clientId() const
{
return m_clientId;
}
QLocale JsonContext::locale() const
{
return m_locale;
}
QByteArray JsonContext::token() const
{
return m_token;
}
void JsonContext::setToken(const QByteArray &token)
{
m_token = token;
}

View File

@ -0,0 +1,54 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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 JSONCONTEXT_H
#define JSONCONTEXT_H
#include <QUuid>
#include <QLocale>
class JsonContext
{
public:
JsonContext(const QUuid &clientId, const QLocale &locale);
QUuid clientId() const;
QLocale locale() const;
QByteArray token() const;
void setToken(const QByteArray &token);
private:
QUuid m_clientId;
QLocale m_locale;
QByteArray m_token;
};
#endif // JSONCONTEXT_H

View File

@ -39,6 +39,7 @@
#include <QDateTime>
#include "jsonreply.h"
#include "jsoncontext.h"
class JsonHandler : public QObject
{
@ -108,7 +109,6 @@ protected:
JsonReply *createAsyncReply(const QString &method) const;
private:
void registerObject(const QMetaObject &metaObject);
void registerObject(const QMetaObject &metaObject, const QMetaObject &listMetaObject);

View File

@ -24,6 +24,7 @@ HEADERS += \
devices/devicepairinginfo.h \
devices/deviceactioninfo.h \
devices/browseresult.h \
jsonrpc/jsoncontext.h \
jsonrpc/jsonhandler.h \
jsonrpc/jsonreply.h \
jsonrpc/jsonrpcserver.h \
@ -106,6 +107,7 @@ SOURCES += \
devices/devicepairinginfo.cpp \
devices/deviceactioninfo.cpp \
devices/browseresult.cpp \
jsonrpc/jsoncontext.cpp \
jsonrpc/jsonhandler.cpp \
jsonrpc/jsonreply.cpp \
jsonrpc/jsonrpcserver.cpp \

View File

@ -34,6 +34,7 @@
#include "version.h"
#include "servers/mocktcpserver.h"
#include "usermanager/usermanager.h"
#include "nymeadbusservice.h"
using namespace nymeaserver;
@ -151,6 +152,7 @@ QStringList TestJSONRPC::extractRefs(const QVariant &variant)
void TestJSONRPC::initTestCase()
{
NymeaDBusService::setBusType(QDBusConnection::SessionBus);
NymeaTestBase::initTestCase();
QLoggingCategory::setFilterRules("*.debug=false\n"
// "JsonRpcTraffic.debug=true\n"
@ -182,7 +184,7 @@ void TestJSONRPC::testHandshake()
// Now register push button agent
PushButtonAgent pushButtonAgent;
pushButtonAgent.init();
pushButtonAgent.init(QDBusConnection::SessionBus);
// And now check if it is sent again when calling JSONRPC.Hello
handShake = injectAndWait("JSONRPC.Hello").toMap();
@ -1141,7 +1143,7 @@ void TestJSONRPC::pluginConfigChangeEmitsNotification()
void TestJSONRPC::testPushButtonAuth()
{
PushButtonAgent pushButtonAgent;
pushButtonAgent.init();
pushButtonAgent.init(QDBusConnection::SessionBus);
QVariantMap params;
params.insert("deviceName", "pbtestdevice");
@ -1167,7 +1169,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt()
{
enableNotifications({});
PushButtonAgent pushButtonAgent;
pushButtonAgent.init();
pushButtonAgent.init(QDBusConnection::SessionBus);
// m_clientId is registered in gutTestbase already, just using it here to improve readability of the test
QUuid aliceId = m_clientId;
@ -1272,7 +1274,7 @@ void TestJSONRPC::testPushButtonAuthInterrupt()
void TestJSONRPC::testPushButtonAuthConnectionDrop()
{
PushButtonAgent pushButtonAgent;
pushButtonAgent.init();
pushButtonAgent.init(QDBusConnection::SessionBus);
// Snoop in on everything the TCP server sends to its clients.
QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
@ -1339,7 +1341,7 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth()
QVERIFY(spy.isValid());
PushButtonAgent pushButtonAgent;
pushButtonAgent.init();
pushButtonAgent.init(QDBusConnection::SessionBus);
// Hello call should work in any case, telling us initial setup is required
spy.clear();

View File

@ -35,6 +35,9 @@
#include "nymeatestbase.h"
#include "usermanager/usermanager.h"
#include "servers/mocktcpserver.h"
#include "nymeadbusservice.h"
#include "../../utils/pushbuttonagent.h"
using namespace nymeaserver;
@ -45,14 +48,50 @@ 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();
@ -79,6 +118,12 @@ TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent)
QCoreApplication::instance()->setOrganizationName("nymea-test");
}
void TestUsermanager::initTestCase()
{
NymeaDBusService::setBusType(QDBusConnection::SessionBus);
NymeaTestBase::initTestCase();
}
void TestUsermanager::init()
{
UserManager *userManager = NymeaCore::instance()->userManager();
@ -155,7 +200,7 @@ void TestUsermanager::authenticate()
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*");
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
QVariant response = injectAndWait("Users.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
@ -163,6 +208,192 @@ void TestUsermanager::authenticate()
QVERIFY2(response.toMap().value("params").toMap().value("success").toString() == "true", "Error authenticating");
}
void TestUsermanager::authenticatePushButton()
{
PushButtonAgent pushButtonAgent;
pushButtonAgent.init(QDBusConnection::SessionBus);
QVariantMap params;
params.insert("deviceName", "pbtestdevice");
QVariant response = injectAndWait("Users.RequestPushButtonAuth", params);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
pushButtonAgent.sendButtonPressed();
if (clientSpy.count() == 0) clientSpy.wait();
QVariantMap rsp = checkNotification(clientSpy, "Users.PushButtonAuthFinished").toMap();
QCOMPARE(rsp.value("params").toMap().value("transactionId").toInt(), transactionId);
QVERIFY2(!rsp.value("params").toMap().value("token").toByteArray().isEmpty(), "Token not in push button auth notification");
}
void TestUsermanager::authenticatePushButtonAuthInterrupt()
{
PushButtonAgent pushButtonAgent;
pushButtonAgent.init(QDBusConnection::SessionBus);
// m_clientId is registered in gutTestbase already, just using it here to improve readability of the test
QUuid aliceId = m_clientId;
// Create a new clientId for mallory and connect it to the server
QUuid malloryId = QUuid::createUuid();
m_mockTcpServer->clientConnected(malloryId);
QSignalSpy responseSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
m_mockTcpServer->injectData(malloryId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (responseSpy.count() == 0) responseSpy.wait();
// Snoop in on everything the TCP server sends to its clients.
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// request push button auth for client 1 (alice) and check for OK reply
QVariantMap params;
params.insert("deviceName", "alice");
QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
int transactionId1 = response.toMap().value("params").toMap().value("transactionId").toInt();
// Request push button auth for client 2 (mallory)
clientSpy.clear();
params.clear();
params.insert("deviceName", "mallory");
response = injectAndWait("Users.RequestPushButtonAuth", params, malloryId);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt();
// Both clients should receive something. Wait for it
if (clientSpy.count() < 2) {
clientSpy.wait();
}
// spy.at(0) should be the failed notification for alice
// spy.at(1) shpuld be the OK reply for mallory
// alice should have received a failed notification. She knows something's wrong.
QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.first().first().toUuid(), aliceId);
QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished"));
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId1);
QCOMPARE(notification.value("params").toMap().value("success").toBool(), false);
// Mallory instead should have received an OK
QVariantMap reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.at(1).first().toUuid(), malloryId);
QCOMPARE(reply.value("params").toMap().value("success").toBool(), true);
// Alice tries once more
clientSpy.clear();
params.clear();
params.insert("deviceName", "alice");
response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
int transactionId3 = response.toMap().value("params").toMap().value("transactionId").toInt();
// Both clients should receive something. Wait for it
if (clientSpy.count() < 2) {
clientSpy.wait();
}
// spy.at(0) should be the failed notification for mallory
// spy.at(1) shpuld be the OK reply for alice
// mallory should have received a failed notification. She knows something's wrong.
notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.first().first().toUuid(), malloryId);
QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished"));
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId2);
QCOMPARE(notification.value("params").toMap().value("success").toBool(), false);
// Alice instead should have received an OK
reply = QJsonDocument::fromJson(clientSpy.at(1).at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.at(1).first().toUuid(), aliceId);
QCOMPARE(reply.value("params").toMap().value("success").toBool(), true);
clientSpy.clear();
// do the button press
pushButtonAgent.sendButtonPressed();
// Wait for things to happen
if (clientSpy.count() == 0) {
clientSpy.wait();
}
// There should have been only exactly one message sent, the token for alice
// Mallory should not have received anything
QCOMPARE(clientSpy.count(), 1);
notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.first().first().toUuid(), aliceId);
QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished"));
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3);
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
}
void TestUsermanager::authenticatePushButtonAuthConnectionDrop()
{
PushButtonAgent pushButtonAgent;
pushButtonAgent.init(QDBusConnection::SessionBus);
// Snoop in on everything the TCP server sends to its clients.
QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
// Create a new clientId for alice and connect it to the server
QUuid aliceId = QUuid::createUuid();
m_mockTcpServer->clientConnected(aliceId);
m_mockTcpServer->injectData(aliceId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait();
// request push button auth for client 1 (alice) and check for OK reply
QVariantMap params;
params.insert("deviceName", "alice");
QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
// Disconnect alice
m_mockTcpServer->clientDisconnected(aliceId);
// Now try with bob
// Create a new clientId for bob and connect it to the server
QUuid bobId = QUuid::createUuid();
m_mockTcpServer->clientConnected(bobId);
clientSpy.clear();
m_mockTcpServer->injectData(bobId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}");
if (clientSpy.count() == 0) clientSpy.wait();
// request push button auth for client 2 (bob) and check for OK reply
params.clear();
params.insert("deviceName", "bob");
response = injectAndWait("Users.RequestPushButtonAuth", params, bobId);
QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true);
int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt();
clientSpy.clear();
pushButtonAgent.sendButtonPressed();
// Wait for things to happen
if (clientSpy.count() == 0) {
clientSpy.wait();
}
// There should have been only exactly one message sent, the token for bob
QCOMPARE(clientSpy.count(), 1);
QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap();
QCOMPARE(clientSpy.first().first().toUuid(), bobId);
QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished"));
QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId);
QCOMPARE(notification.value("params").toMap().value("success").toBool(), true);
QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be");
}
void TestUsermanager::createDuplicateUser()
{
authenticate();
@ -223,7 +454,7 @@ void TestUsermanager::authenticateAfterPasswordChangeOK()
params.insert("username", "valid@user.test");
params.insert("password", "Blubb123"); // New password, should be ok
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
QVariant response = injectAndWait("Users.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
QVERIFY2(!m_apiToken.isEmpty(), "Token should not be empty");
@ -239,7 +470,7 @@ void TestUsermanager::authenticateAfterPasswordChangeFail()
params.insert("username", "valid@user.test");
params.insert("password", "Bla1234*"); // Original password, should not be ok
params.insert("deviceName", "autotests");
QVariant response = injectAndWait("JSONRPC.Authenticate", params);
QVariant response = injectAndWait("Users.Authenticate", params);
m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray();
QVERIFY2(m_apiToken.isEmpty(), "Token should be empty");

View File

@ -2,4 +2,9 @@ include(../../../nymea.pri)
include(../autotests.pri)
TARGET = testusermanager
SOURCES += testusermanager.cpp
HEADERS += ../../utils/pushbuttonagent.h
SOURCES += testusermanager.cpp \
../../utils/pushbuttonagent.cpp