Merge PR #341: Add an API keys provider plugin mechanism

This commit is contained in:
Jenkins nymea 2020-10-28 19:07:39 +01:00
commit c956988f32
17 changed files with 298 additions and 4 deletions

1
debian/control vendored
View File

@ -75,6 +75,7 @@ Recommends: nymea-cli,
nymea-update-plugin-impl,
nymea-system-plugin-impl,
nymea-zeroconf-plugin-impl,
nymea-apikeysprovider-plugin-impl,
Replaces: guhd
Description: An open source IoT server - daemon
The nymea daemon is a plugin based IoT (Internet of Things) server. The

View File

@ -0,0 +1,77 @@
#include "apikeysprovidersloader.h"
#include <QCoreApplication>
#include <QDir>
#include <QPluginLoader>
ApiKeysProvidersLoader::ApiKeysProvidersLoader(QObject *parent):
QObject(parent)
{
foreach (const QString &path, pluginSearchDirs()) {
QDir dir(path);
qCDebug(dcApiKeys()) << "Loading API keys provider plugins from:" << dir.absolutePath();
foreach (const QString &entry, dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)) {
QFileInfo fi(path + "/" + entry);
if (fi.isFile()) {
if (entry.startsWith("libnymea_apikeysproviderplugin") && entry.endsWith(".so")) {
loadPlugin(path + "/" + entry);
}
} else if (fi.isDir()) {
if (QFileInfo::exists(path + "/" + entry + "/libnymea_apikeysproviderplugin" + entry + ".so")) {
loadPlugin(path + "/" + entry + "/libnymea_apikeysproviderplugin" + entry + ".so");
}
}
}
}
}
QHash<QString, ApiKey> ApiKeysProvidersLoader::allApiKeys() const
{
QHash<QString, ApiKey> ret;
foreach (ApiKeysProvider *provider, m_providers) {
foreach (const QString &name, provider->apiKeys().keys()) {
ret.insert(name, provider->apiKeys().value(name));
}
}
return ret;
}
QStringList ApiKeysProvidersLoader::pluginSearchDirs() const
{
QStringList searchDirs;
QByteArray envPath = qgetenv("NYMEA_APIKEYS_PLUGINS_PATH");
if (!envPath.isEmpty()) {
searchDirs << QString(envPath).split(':');
}
foreach (QString libraryPath, QCoreApplication::libraryPaths()) {
searchDirs << libraryPath.replace("qt5", "nymea").replace("plugins", "apikeysproviders");
}
searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../lib/nymea/apikeysproviders/").absolutePath();
searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../apikeysproviders/").absolutePath();
searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../../../apikeysproviders/").absolutePath();
searchDirs.removeDuplicates();
return searchDirs;
}
void ApiKeysProvidersLoader::loadPlugin(const QString &file)
{
QPluginLoader loader;
loader.setFileName(file);
loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
if (!loader.load()) {
qCWarning(dcApiKeys()) << loader.errorString();
return;
}
ApiKeysProvider *provider = qobject_cast<ApiKeysProvider*>(loader.instance());
if (!provider) {
qCWarning(dcApiKeys()) << "Could not get plugin instance of" << loader.fileName();
loader.unload();
return;
}
qCDebug(dcApiKeys()) << "Loaded API keys provider plugin:" << loader.fileName();
provider->setParent(this);
m_providers.append(provider);
}

View File

@ -0,0 +1,24 @@
#ifndef APIKEYSPROVIDERSLOADER_H
#define APIKEYSPROVIDERSLOADER_H
#include "network/apikeys/apikeysprovider.h"
class ApiKeysProvidersLoader: public QObject
{
Q_OBJECT
public:
ApiKeysProvidersLoader(QObject *parent = nullptr);
QList<ApiKeysProvider*> providers() const;
QHash<QString, ApiKey> allApiKeys() const;
private:
QStringList pluginSearchDirs() const;
void loadPlugin(const QString &file);
QList<ApiKeysProvider*> m_providers;
};
#endif // APIKEYSPROVIDERSLOADER_H

View File

@ -52,6 +52,8 @@
#include "integrations/browseractioninfo.h"
#include "integrations/browseritemactioninfo.h"
#include "apikeysprovidersloader.h"
//#include "unistd.h"
#include "plugintimer.h"
@ -86,6 +88,8 @@ ThingManagerImplementation::ThingManagerImplementation(HardwareManager *hardware
oldStateFile.copy(settingsPath + "/thingstates.conf");
}
m_apiKeysProvidersLoader = new ApiKeysProvidersLoader(this);
// Give hardware a chance to start up before loading plugins etc.
QMetaObject::invokeMethod(this, "loadPlugins", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, "loadConfiguredThings", Qt::QueuedConnection);
@ -1319,8 +1323,28 @@ void ThingManagerImplementation::loadPlugins()
void ThingManagerImplementation::loadPlugin(IntegrationPlugin *pluginIface)
{
// Populate the API storage for the plugin.
// NOTE:
// Right now we grant access to every api key requested in the JSON file. This means, an attacker could just
// write a plugin requesting a certain key and load it. This is not an actual problem right now as
// deployments that allow loading random plugins don't ship any high security keys. Once nymea supports
// a "plugin store" and allows loading 3rd party plugins along with a more sensitive api key provider,
// the plugins JSON needs to be reviewd by the store owner and signed with a store key. Only signed plugins
// should be granted access to their requested keys.
ApiKeyStorage *apiKeyStorage = new ApiKeyStorage(pluginIface);
QStringList requestedKeys = pluginIface->metadata().apiKeys();
foreach (const QString &apiKeyName, pluginIface->metadata().apiKeys()) {
if (m_apiKeysProvidersLoader->allApiKeys().contains(apiKeyName)) {
ApiKey apiKey = m_apiKeysProvidersLoader->allApiKeys().value(apiKeyName);
apiKeyStorage->insertKey(apiKeyName, apiKey);
requestedKeys.removeAll(apiKeyName);
}
}
if (!requestedKeys.isEmpty()) {
qCWarning(dcThingManager()).nospace() << "Unable to load API keys for plugin " << pluginIface->metadata().pluginName() << ": " << requestedKeys;
}
pluginIface->setParent(this);
pluginIface->initPlugin(this, m_hardwareManager);
pluginIface->initPlugin(this, m_hardwareManager, apiKeyStorage);
qCDebug(dcThingManager) << "**** Loaded plugin" << pluginIface->pluginName();
foreach (const Vendor &vendor, pluginIface->supportedVendors()) {

View File

@ -59,6 +59,7 @@ class IntegrationPlugin;
class ThingPairingInfo;
class HardwareManager;
class Translator;
class ApiKeysProvidersLoader;
class ThingManagerImplementation: public ThingManager
{
@ -192,6 +193,8 @@ private:
QHash<PairingTransactionId, PairingContext> m_pendingPairings;
QHash<IOConnectionId, IOConnection> m_ioConnections;
ApiKeysProvidersLoader *m_apiKeysProvidersLoader = nullptr;
};
#endif // THINGMANAGERIMPLEMENTATION_H

View File

@ -36,6 +36,7 @@ RESOURCES += $$top_srcdir/icons.qrc \
HEADERS += nymeacore.h \
integrations/apikeysprovidersloader.h \
integrations/plugininfocache.h \
integrations/python/pynymealogginghandler.h \
integrations/python/pynymeamodule.h \
@ -129,6 +130,7 @@ HEADERS += nymeacore.h \
SOURCES += nymeacore.cpp \
integrations/apikeysprovidersloader.cpp \
integrations/plugininfocache.cpp \
integrations/thingmanagerimplementation.cpp \
integrations/translator.cpp \

View File

@ -374,10 +374,11 @@ ParamTypes IntegrationPlugin::configurationDescription() const
return m_metaData.pluginSettings();
}
void IntegrationPlugin::initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager)
void IntegrationPlugin::initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager, ApiKeyStorage *apiKeyStorage)
{
m_thingManager = thingManager;
m_hardwareManager = hardwareManager;
m_apiKeyStorage = apiKeyStorage;
m_storage = new QSettings(NymeaSettings::settingsPath() + "/pluginconfig-" + pluginId().toString().remove(QRegExp("[{}]")) + ".conf", QSettings::IniFormat, this);
}
@ -478,6 +479,15 @@ QSettings* IntegrationPlugin::pluginStorage() const
return m_storage;
}
/*!
* \brief IntegrationPlugin::apiKeyStorage
* \return Returns the api key storage for this plugin. A plugin needs to list required API keys in the plugins JSON file.
*/
ApiKeyStorage *IntegrationPlugin::apiKeyStorage() const
{
return m_apiKeyStorage;
}
void IntegrationPlugin::setMetaData(const PluginMetadata &metaData)
{
m_metaData = metaData;

View File

@ -49,6 +49,8 @@
#include "hardwaremanager.h"
#include "network/apikeys/apikeystorage.h"
#include "thingdiscoveryinfo.h"
#include "thingpairinginfo.h"
#include "thingsetupinfo.h"
@ -125,6 +127,7 @@ protected:
Things myThings() const;
HardwareManager *hardwareManager() const;
QSettings *pluginStorage() const;
ApiKeyStorage *apiKeyStorage() const;
void setMetaData(const PluginMetadata &metaData);
@ -132,12 +135,13 @@ private:
friend class ThingManager;
friend class ThingManagerImplementation;
void initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager);
void initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager, ApiKeyStorage *apiKeyStorage);
PluginMetadata m_metaData;
ThingManager *m_thingManager = nullptr;
HardwareManager *m_hardwareManager = nullptr;
QSettings *m_storage = nullptr;
ApiKeyStorage *m_apiKeyStorage;
ParamList m_config;
};

View File

@ -84,6 +84,11 @@ bool PluginMetadata::isBuiltIn() const
return m_isBuiltIn;
}
QStringList PluginMetadata::apiKeys() const
{
return m_apiKeys;
}
ParamTypes PluginMetadata::pluginSettings() const
{
return m_pluginSettings;
@ -110,7 +115,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
// General plugin info
QStringList pluginMandatoryJsonProperties = QStringList() << "id" << "name" << "displayName" << "vendors";
QStringList pluginJsonProperties = QStringList() << "id" << "name" << "displayName" << "vendors" << "paramTypes" << "builtIn";
QStringList pluginJsonProperties = QStringList() << "id" << "name" << "displayName" << "vendors" << "paramTypes" << "builtIn" << "apiKeys";
QPair<QStringList, QStringList> verificationResult = verifyFields(pluginJsonProperties, pluginMandatoryJsonProperties, jsonObject);
if (!verificationResult.first.isEmpty()) {
m_validationErrors.append("Plugin metadata has missing fields: " + verificationResult.first.join(", "));
@ -122,6 +127,9 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
m_pluginId = PluginId(jsonObject.value("id").toString());
m_pluginName = jsonObject.value("name").toString();
m_pluginDisplayName = jsonObject.value("displayName").toString();
foreach (const QVariant &apiKeyVariant, jsonObject.value("apiKeys").toArray().toVariantList()) {
m_apiKeys.append(apiKeyVariant.toString());
}
if (!verificationResult.second.isEmpty()) {
m_validationErrors.append("Plugin \"" + m_pluginName + "\" has unknown fields: \"" + verificationResult.second.join("\", \"") + "\"");

View File

@ -49,6 +49,7 @@ public:
QString pluginName() const;
QString pluginDisplayName() const;
bool isBuiltIn() const;
QStringList apiKeys() const;
ParamTypes pluginSettings() const;
@ -76,6 +77,7 @@ private:
ParamTypes m_pluginSettings;
Vendors m_vendors;
ThingClasses m_thingClasses;
QStringList m_apiKeys;
QList<QUuid> m_allUuids;

View File

@ -30,6 +30,9 @@ HEADERS += \
jsonrpc/jsonreply.h \
jsonrpc/jsonrpcserver.h \
libnymea.h \
network/apikeys/apikey.h \
network/apikeys/apikeysprovider.h \
network/apikeys/apikeystorage.h \
platform/package.h \
platform/repository.h \
types/browseritem.h \
@ -119,6 +122,9 @@ SOURCES += \
jsonrpc/jsonreply.cpp \
jsonrpc/jsonrpcserver.cpp \
loggingcategories.cpp \
network/apikeys/apikey.cpp \
network/apikeys/apikeysprovider.cpp \
network/apikeys/apikeystorage.cpp \
nymeasettings.cpp \
platform/package.cpp \
platform/repository.cpp \

View File

@ -0,0 +1,32 @@
#include "apikey.h"
#include "loggingcategories.h"
NYMEA_LOGGING_CATEGORY(dcApiKeys, "ApiKeys")
ApiKey::ApiKey()
{
}
/*!
* \brief ApiKey::data
* \param key
* \return Retrns the data for key. For example data("key") or data("clientId")
* An ApiKey can have multiple properties, like appid, clientsecret, scope information etc.
*/
QByteArray ApiKey::data(const QString &key) const
{
return m_data.value(key);
}
/*!
* \brief ApiKey::insert
* Insert a key value pair in the this api key. For example insert("appid", "...").
* An ApiKey can have multiple properties, like appid, clientsecret, scope information etc.
* \param key
* \param data
*/
void ApiKey::insert(const QString &key, const QByteArray &data)
{
m_data.insert(key, data);
}

View File

@ -0,0 +1,22 @@
#ifndef APIKEY_H
#define APIKEY_H
#include <QString>
#include <QHash>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcApiKeys)
class ApiKey
{
public:
ApiKey();
QByteArray data(const QString &key) const;
void insert(const QString &key, const QByteArray &data);
private:
QHash<QString, QByteArray> m_data;
};
#endif // APIKEY_H

View File

@ -0,0 +1,7 @@
#include "apikeysprovider.h"
ApiKeysProvider::ApiKeysProvider(QObject *parent):
QObject(parent)
{
}

View File

@ -0,0 +1,21 @@
#ifndef APIKEYSPROVIDER_H
#define APIKEYSPROVIDER_H
#include "apikey.h"
#include <QObject>
class ApiKeysProvider: public QObject
{
Q_OBJECT
public:
ApiKeysProvider(QObject *parent = nullptr);
virtual ~ApiKeysProvider() = default;
virtual QHash<QString, ApiKey> apiKeys() const = 0;
};
Q_DECLARE_INTERFACE(ApiKeysProvider, "io.nymea.ApiKeysProvider")
#endif // APIKEYSPROVIDER_H

View File

@ -0,0 +1,26 @@
#include "apikeystorage.h"
ApiKeyStorage::ApiKeyStorage(QObject *parent):
QObject(parent)
{
}
ApiKey ApiKeyStorage::requestKey(const QString &name) const
{
if (!m_keys.contains(name)) {
qCWarning(dcApiKeys) << "API key not found for" << name;
}
return m_keys.value(name);
}
void ApiKeyStorage::insertKey(const QString &name, const ApiKey &key)
{
if (m_keys.contains(name)) {
m_keys[name] = key;
emit keyUpdated(name, key);
} else {
m_keys.insert(name, key);
emit keyAdded(name, key);
}
}

View File

@ -0,0 +1,25 @@
#ifndef APIKEYSTORAGE_H
#define APIKEYSTORAGE_H
#include "apikey.h"
#include <QObject>
class ApiKeyStorage: public QObject
{
Q_OBJECT
public:
ApiKeyStorage(QObject *parent = nullptr);
ApiKey requestKey(const QString &name) const;
void insertKey(const QString &name, const ApiKey &key);
signals:
void keyAdded(const QString &name, const ApiKey &key);
void keyUpdated(const QString &name, const ApiKey &key);
private:
QHash<QString, ApiKey> m_keys;
};
#endif // APIKEYSTORAGE_H