Merge PR #341: Add an API keys provider plugin mechanism
This commit is contained in:
commit
c956988f32
1
debian/control
vendored
1
debian/control
vendored
@ -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
|
||||
|
||||
77
libnymea-core/integrations/apikeysprovidersloader.cpp
Normal file
77
libnymea-core/integrations/apikeysprovidersloader.cpp
Normal 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);
|
||||
|
||||
}
|
||||
24
libnymea-core/integrations/apikeysprovidersloader.h
Normal file
24
libnymea-core/integrations/apikeysprovidersloader.h
Normal 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
|
||||
@ -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()) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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("\", \"") + "\"");
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 \
|
||||
|
||||
32
libnymea/network/apikeys/apikey.cpp
Normal file
32
libnymea/network/apikeys/apikey.cpp
Normal 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);
|
||||
}
|
||||
22
libnymea/network/apikeys/apikey.h
Normal file
22
libnymea/network/apikeys/apikey.h
Normal 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
|
||||
7
libnymea/network/apikeys/apikeysprovider.cpp
Normal file
7
libnymea/network/apikeys/apikeysprovider.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include "apikeysprovider.h"
|
||||
|
||||
ApiKeysProvider::ApiKeysProvider(QObject *parent):
|
||||
QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
21
libnymea/network/apikeys/apikeysprovider.h
Normal file
21
libnymea/network/apikeys/apikeysprovider.h
Normal 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
|
||||
26
libnymea/network/apikeys/apikeystorage.cpp
Normal file
26
libnymea/network/apikeys/apikeystorage.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
25
libnymea/network/apikeys/apikeystorage.h
Normal file
25
libnymea/network/apikeys/apikeystorage.h
Normal 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
|
||||
Reference in New Issue
Block a user