diff --git a/debian/control b/debian/control index e96a8e81..5f96866a 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/libnymea-core/integrations/apikeysprovidersloader.cpp b/libnymea-core/integrations/apikeysprovidersloader.cpp new file mode 100644 index 00000000..6d9e9fb3 --- /dev/null +++ b/libnymea-core/integrations/apikeysprovidersloader.cpp @@ -0,0 +1,77 @@ +#include "apikeysprovidersloader.h" + +#include +#include +#include + +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 ApiKeysProvidersLoader::allApiKeys() const +{ + QHash 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(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); + +} diff --git a/libnymea-core/integrations/apikeysprovidersloader.h b/libnymea-core/integrations/apikeysprovidersloader.h new file mode 100644 index 00000000..bfe9a85f --- /dev/null +++ b/libnymea-core/integrations/apikeysprovidersloader.h @@ -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 providers() const; + + QHash allApiKeys() const; + +private: + QStringList pluginSearchDirs() const; + void loadPlugin(const QString &file); + + QList m_providers; +}; + +#endif // APIKEYSPROVIDERSLOADER_H diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index 19a2e2ff..fa164ab3 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -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()) { diff --git a/libnymea-core/integrations/thingmanagerimplementation.h b/libnymea-core/integrations/thingmanagerimplementation.h index c127eef7..e8628e81 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.h +++ b/libnymea-core/integrations/thingmanagerimplementation.h @@ -59,6 +59,7 @@ class IntegrationPlugin; class ThingPairingInfo; class HardwareManager; class Translator; +class ApiKeysProvidersLoader; class ThingManagerImplementation: public ThingManager { @@ -192,6 +193,8 @@ private: QHash m_pendingPairings; QHash m_ioConnections; + + ApiKeysProvidersLoader *m_apiKeysProvidersLoader = nullptr; }; #endif // THINGMANAGERIMPLEMENTATION_H diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index f5a2063c..0b00372f 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -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 \ diff --git a/libnymea/integrations/integrationplugin.cpp b/libnymea/integrations/integrationplugin.cpp index 3f15c8c9..a475c008 100644 --- a/libnymea/integrations/integrationplugin.cpp +++ b/libnymea/integrations/integrationplugin.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; diff --git a/libnymea/integrations/integrationplugin.h b/libnymea/integrations/integrationplugin.h index daeb4b06..ce1ca8b0 100644 --- a/libnymea/integrations/integrationplugin.h +++ b/libnymea/integrations/integrationplugin.h @@ -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; }; diff --git a/libnymea/integrations/pluginmetadata.cpp b/libnymea/integrations/pluginmetadata.cpp index a10000ba..65a9f024 100644 --- a/libnymea/integrations/pluginmetadata.cpp +++ b/libnymea/integrations/pluginmetadata.cpp @@ -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 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("\", \"") + "\""); diff --git a/libnymea/integrations/pluginmetadata.h b/libnymea/integrations/pluginmetadata.h index 6fb5861b..0ba68cfc 100644 --- a/libnymea/integrations/pluginmetadata.h +++ b/libnymea/integrations/pluginmetadata.h @@ -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 m_allUuids; diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index 2da85aad..b86c15e8 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -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 \ diff --git a/libnymea/network/apikeys/apikey.cpp b/libnymea/network/apikeys/apikey.cpp new file mode 100644 index 00000000..de54bd0f --- /dev/null +++ b/libnymea/network/apikeys/apikey.cpp @@ -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); +} diff --git a/libnymea/network/apikeys/apikey.h b/libnymea/network/apikeys/apikey.h new file mode 100644 index 00000000..3e58304f --- /dev/null +++ b/libnymea/network/apikeys/apikey.h @@ -0,0 +1,22 @@ +#ifndef APIKEY_H +#define APIKEY_H + +#include +#include + +#include +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 m_data; +}; + +#endif // APIKEY_H diff --git a/libnymea/network/apikeys/apikeysprovider.cpp b/libnymea/network/apikeys/apikeysprovider.cpp new file mode 100644 index 00000000..68c72309 --- /dev/null +++ b/libnymea/network/apikeys/apikeysprovider.cpp @@ -0,0 +1,7 @@ +#include "apikeysprovider.h" + +ApiKeysProvider::ApiKeysProvider(QObject *parent): + QObject(parent) +{ + +} diff --git a/libnymea/network/apikeys/apikeysprovider.h b/libnymea/network/apikeys/apikeysprovider.h new file mode 100644 index 00000000..92044279 --- /dev/null +++ b/libnymea/network/apikeys/apikeysprovider.h @@ -0,0 +1,21 @@ +#ifndef APIKEYSPROVIDER_H +#define APIKEYSPROVIDER_H + +#include "apikey.h" + +#include + +class ApiKeysProvider: public QObject +{ + Q_OBJECT + +public: + ApiKeysProvider(QObject *parent = nullptr); + virtual ~ApiKeysProvider() = default; + + virtual QHash apiKeys() const = 0; +}; + +Q_DECLARE_INTERFACE(ApiKeysProvider, "io.nymea.ApiKeysProvider") + +#endif // APIKEYSPROVIDER_H diff --git a/libnymea/network/apikeys/apikeystorage.cpp b/libnymea/network/apikeys/apikeystorage.cpp new file mode 100644 index 00000000..bd54ff34 --- /dev/null +++ b/libnymea/network/apikeys/apikeystorage.cpp @@ -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); + } +} diff --git a/libnymea/network/apikeys/apikeystorage.h b/libnymea/network/apikeys/apikeystorage.h new file mode 100644 index 00000000..27213799 --- /dev/null +++ b/libnymea/network/apikeys/apikeystorage.h @@ -0,0 +1,25 @@ +#ifndef APIKEYSTORAGE_H +#define APIKEYSTORAGE_H + +#include "apikey.h" + +#include + +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 m_keys; +}; + +#endif // APIKEYSTORAGE_H