diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 8b2b16cf..3b3adbfb 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -173,7 +173,8 @@ static const QHash configStrings = { // FdoSecrets {Config::FdoSecrets_Enabled, {QS("FdoSecrets/Enabled"), Roaming, false}}, {Config::FdoSecrets_ShowNotification, {QS("FdoSecrets/ShowNotification"), Roaming, true}}, - {Config::FdoSecrets_NoConfirmDeleteItem, {QS("FdoSecrets/NoConfirmDeleteItem"), Roaming, false}}, + {Config::FdoSecrets_ConfirmDeleteItem, {QS("FdoSecrets/ConfirmDeleteItem"), Roaming, true}}, + {Config::FdoSecrets_ConfirmAccessItem, {QS("FdoSecrets/ConfirmAccessItem"), Roaming, true}}, // KeeShare {Config::KeeShare_QuietSuccess, {QS("KeeShare/QuietSuccess"), Roaming, false}}, diff --git a/src/core/Config.h b/src/core/Config.h index 8b9a02a5..fc8af88e 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -151,7 +151,8 @@ public: FdoSecrets_Enabled, FdoSecrets_ShowNotification, - FdoSecrets_NoConfirmDeleteItem, + FdoSecrets_ConfirmDeleteItem, + FdoSecrets_ConfirmAccessItem, KeeShare_QuietSuccess, KeeShare_Own, diff --git a/src/core/Global.h b/src/core/Global.h index aebdb455..5e737514 100644 --- a/src/core/Global.h +++ b/src/core/Global.h @@ -53,6 +53,15 @@ enum IconSize Large }; +enum class AuthDecision +{ + Undecided, + Allowed, + AllowedOnce, + Denied, + DeniedOnce, +}; + template struct AddConst { typedef const T Type; diff --git a/src/fdosecrets/CMakeLists.txt b/src/fdosecrets/CMakeLists.txt index a9750bc2..35728a17 100644 --- a/src/fdosecrets/CMakeLists.txt +++ b/src/fdosecrets/CMakeLists.txt @@ -6,11 +6,15 @@ if(WITH_XC_FDOSECRETS) FdoSecretsPlugin.cpp widgets/SettingsModels.cpp widgets/SettingsWidgetFdoSecrets.cpp + widgets/RowButtonHelper.cpp # per database settings page DatabaseSettingsPageFdoSecrets.cpp widgets/DatabaseSettingsWidgetFdoSecrets.cpp + # prompt dialog + widgets/AccessControlDialog.cpp + # setting storage FdoSecretsSettings.cpp @@ -18,20 +22,17 @@ if(WITH_XC_FDOSECRETS) GcryptMPI.cpp # dbus objects - objects/DBusObject.cpp + dbus/DBusClient.cpp + dbus/DBusMgr.cpp + dbus/DBusDispatch.cpp + dbus/DBusObject.cpp objects/Service.cpp objects/Session.cpp objects/SessionCipher.cpp objects/Collection.cpp objects/Item.cpp objects/Prompt.cpp - objects/adaptors/ServiceAdaptor.cpp - objects/adaptors/SessionAdaptor.cpp - objects/adaptors/CollectionAdaptor.cpp - objects/adaptors/ItemAdaptor.cpp - objects/adaptors/PromptAdaptor.cpp - objects/DBusReturn.cpp - objects/DBusTypes.cpp + dbus/DBusTypes.cpp ) target_link_libraries(fdosecrets Qt5::Core Qt5::Widgets Qt5::DBus ${GCRYPT_LIBRARIES}) endif() diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp index 8004de24..20247a57 100644 --- a/src/fdosecrets/FdoSecretsPlugin.cpp +++ b/src/fdosecrets/FdoSecretsPlugin.cpp @@ -18,14 +18,14 @@ #include "FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" -#include "fdosecrets/objects/DBusTypes.h" +#include "fdosecrets/dbus/DBusMgr.h" +#include "fdosecrets/dbus/DBusTypes.h" #include "fdosecrets/objects/Service.h" #include "fdosecrets/widgets/SettingsWidgetFdoSecrets.h" #include "gui/DatabaseTabWidget.h" -#include - +using FdoSecrets::DBusMgr; using FdoSecrets::Service; // TODO: Only used for testing. Need to split service functions away from settings page. @@ -33,9 +33,13 @@ QPointer g_fdoSecretsPlugin; FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget) : m_dbTabs(tabWidget) + , m_dbus(new DBusMgr()) { + registerDBusTypes(m_dbus); + m_dbus->populateMethodCache(); + + connect(m_dbus.data(), &DBusMgr::error, this, &FdoSecretsPlugin::emitError); g_fdoSecretsPlugin = this; - FdoSecrets::registerDBusTypes(); } FdoSecretsPlugin* FdoSecretsPlugin::getPlugin() @@ -63,7 +67,7 @@ void FdoSecretsPlugin::updateServiceState() { if (FdoSecrets::settings()->isEnabled()) { if (!m_secretService && m_dbTabs) { - m_secretService = Service::Create(this, m_dbTabs); + m_secretService = Service::Create(this, m_dbTabs, m_dbus); if (!m_secretService) { FdoSecrets::settings()->setEnabled(false); return; @@ -88,6 +92,11 @@ DatabaseTabWidget* FdoSecretsPlugin::dbTabs() const return m_dbTabs; } +const QSharedPointer& FdoSecretsPlugin::dbus() const +{ + return m_dbus; +} + void FdoSecretsPlugin::emitRequestSwitchToDatabases() { emit requestSwitchToDatabases(); @@ -106,29 +115,3 @@ void FdoSecretsPlugin::emitError(const QString& msg) emit error(tr("Fdo Secret Service: %1").arg(msg)); qDebug() << msg; } - -QString FdoSecretsPlugin::reportExistingService() const -{ - auto pidStr = tr("Unknown", "Unknown PID"); - auto exeStr = tr("Unknown", "Unknown executable path"); - - // try get pid - auto pid = QDBusConnection::sessionBus().interface()->servicePid(DBUS_SERVICE_SECRET); - if (pid.isValid()) { - pidStr = QString::number(pid.value()); - - // try get the first part of the cmdline, which usually is the executable name/path - QFile proc(QStringLiteral("/proc/%1/cmdline").arg(pid.value())); - if (proc.open(QFile::ReadOnly)) { - auto parts = proc.readAll().split('\0'); - if (parts.length() >= 1) { - exeStr = QString::fromLocal8Bit(parts[0]).trimmed(); - } - } - } - auto otherService = tr("PID: %1, Executable: %2", "PID: 1234, Executable: /path/to/exe") - .arg(pidStr, exeStr.toHtmlEscaped()); - return tr("Another secret service is running (%1).
" - "Please stop/remove it before re-enabling the Secret Service Integration.") - .arg(otherService); -} diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h index 28233460..13f8669f 100644 --- a/src/fdosecrets/FdoSecretsPlugin.h +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -30,6 +30,7 @@ class DatabaseTabWidget; namespace FdoSecrets { class Service; + class DBusMgr; } // namespace FdoSecrets class FdoSecretsPlugin : public QObject, public ISettingsPage @@ -66,10 +67,10 @@ public: DatabaseTabWidget* dbTabs() const; /** - * Check the running secret service and returns info about it - * @return html string suitable to be shown in the UI + * @brief The dbus manager instance + * @return */ - QString reportExistingService() const; + const QSharedPointer& dbus() const; // TODO: Only used for testing. Need to split service functions away from settings page. static FdoSecretsPlugin* getPlugin(); @@ -93,6 +94,7 @@ signals: private: QPointer m_dbTabs; + QSharedPointer m_dbus; QSharedPointer m_secretService; }; diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp index 20eff4a0..c2ebf9d4 100644 --- a/src/fdosecrets/FdoSecretsSettings.cpp +++ b/src/fdosecrets/FdoSecretsSettings.cpp @@ -64,14 +64,24 @@ namespace FdoSecrets config()->set(Config::FdoSecrets_ShowNotification, show); } - bool FdoSecretsSettings::noConfirmDeleteItem() const + bool FdoSecretsSettings::confirmDeleteItem() const { - return config()->get(Config::FdoSecrets_NoConfirmDeleteItem).toBool(); + return config()->get(Config::FdoSecrets_ConfirmDeleteItem).toBool(); } - void FdoSecretsSettings::setNoConfirmDeleteItem(bool noConfirm) + void FdoSecretsSettings::setConfirmDeleteItem(bool confirm) { - config()->set(Config::FdoSecrets_NoConfirmDeleteItem, noConfirm); + config()->set(Config::FdoSecrets_ConfirmDeleteItem, confirm); + } + + bool FdoSecretsSettings::confirmAccessItem() const + { + return config()->get(Config::FdoSecrets_ConfirmAccessItem).toBool(); + } + + void FdoSecretsSettings::setConfirmAccessItem(bool confirmAccessItem) + { + config()->set(Config::FdoSecrets_ConfirmAccessItem, confirmAccessItem); } QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer& db) const diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h index 5a902887..24a37a8d 100644 --- a/src/fdosecrets/FdoSecretsSettings.h +++ b/src/fdosecrets/FdoSecretsSettings.h @@ -38,8 +38,11 @@ namespace FdoSecrets bool showNotification() const; void setShowNotification(bool show); - bool noConfirmDeleteItem() const; - void setNoConfirmDeleteItem(bool noConfirm); + bool confirmDeleteItem() const; + void setConfirmDeleteItem(bool confirm); + + bool confirmAccessItem() const; + void setConfirmAccessItem(bool confirmAccessItem); // Per db settings diff --git a/src/fdosecrets/dbus/DBusClient.cpp b/src/fdosecrets/dbus/DBusClient.cpp new file mode 100644 index 00000000..4fa47465 --- /dev/null +++ b/src/fdosecrets/dbus/DBusClient.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 Aetf + * Copyright (C) 2020 Jan Klötzke + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusClient.h" + +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" +#include "fdosecrets/objects/SessionCipher.h" + +namespace FdoSecrets +{ + DBusClient::DBusClient(DBusMgr* dbus, const QString& address, uint pid, const QString& name) + : m_dbus(dbus) + , m_address(address) + , m_pid(pid) + , m_name(name) + { + } + + bool DBusClient::itemKnown(const QUuid& uuid) const + { + return m_authorizedAll || m_allowed.contains(uuid) || m_allowedOnce.contains(uuid) || m_denied.contains(uuid) + || m_deniedOnce.contains(uuid); + } + + bool DBusClient::itemAuthorized(const QUuid& uuid) const + { + if (!FdoSecrets::settings()->confirmAccessItem()) { + // everyone is authorized if this is not enabled + return true; + } + if (m_authorizedAll) { + // this client is trusted + return true; + } + if (m_deniedOnce.contains(uuid) || m_denied.contains(uuid)) { + // explicitly denied + return false; + } + if (m_allowedOnce.contains(uuid) || m_allowed.contains(uuid)) { + // explicitly allowed + return true; + } + // haven't asked, not authorized by default + return false; + } + + bool DBusClient::itemAuthorizedResetOnce(const QUuid& uuid) + { + auto auth = itemAuthorized(uuid); + m_deniedOnce.remove(uuid); + m_allowedOnce.remove(uuid); + return auth; + } + + void DBusClient::setItemAuthorized(const QUuid& uuid, AuthDecision auth) + { + // uuid should only be in exactly one set at any time + m_allowed.remove(uuid); + m_allowedOnce.remove(uuid); + m_denied.remove(uuid); + m_deniedOnce.remove(uuid); + switch (auth) { + case AuthDecision::Allowed: + m_allowed.insert(uuid); + break; + case AuthDecision::AllowedOnce: + m_allowedOnce.insert(uuid); + break; + case AuthDecision::Denied: + m_denied.insert(uuid); + break; + case AuthDecision::DeniedOnce: + m_deniedOnce.insert(uuid); + break; + default: + break; + } + } + + void DBusClient::setAllAuthorized(bool authorized) + { + m_authorizedAll = authorized; + } + + void DBusClient::clearAuthorization() + { + m_authorizedAll = false; + m_allowed.clear(); + m_allowedOnce.clear(); + m_denied.clear(); + m_deniedOnce.clear(); + } + + void DBusClient::disconnectDBus() + { + clearAuthorization(); + // notify DBusMgr about the removal + m_dbus->removeClient(this); + } + + QSharedPointer + DBusClient::negotiateCipher(const QString& algorithm, const QVariant& input, QVariant& output, bool& incomplete) + { + incomplete = false; + + QSharedPointer cipher{}; + if (algorithm == PlainCipher::Algorithm) { + cipher.reset(new PlainCipher); + } else if (algorithm == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { + QByteArray clientPublicKey = input.toByteArray(); + cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey)); + } else { + // error notSupported + } + + if (!cipher) { + return {}; + } + + if (!cipher->isValid()) { + qWarning() << "FdoSecrets: Error creating cipher"; + return {}; + } + + output = cipher->negotiationOutput(); + return cipher; + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusClient.h b/src/fdosecrets/dbus/DBusClient.h new file mode 100644 index 00000000..994a9d4f --- /dev/null +++ b/src/fdosecrets/dbus/DBusClient.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 Aetf + * Copyright (C) 2020 Jan Klötzke + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSCLIENT_H +#define KEEPASSXC_FDOSECRETS_DBUSCLIENT_H + +#include +#include +#include +#include +#include + +#include "core/Global.h" + +namespace FdoSecrets +{ + class DBusMgr; + class CipherPair; + + /** + * Represent a client that has made requests to our service. A client is identified by its + * DBus address, which is guaranteed to be unique by the DBus protocol. + * + * An object of this class is created on the first request and destroyed + * when the client address vanishes from the bus. DBus guarantees that the + * client address is not reused. + * + * One client may have multiple `Session`s with our service, and this class + * manages the negotiation state (if any) of ciphers and per-client authorization + * status. + */ + class DBusClient + { + public: + /** + * @brief Given peer's service address, construct a client object + * @param address obtained from `QDBusMessage::service()` + * @param pid the process PID + * @param name the process name + */ + explicit DBusClient(DBusMgr* dbus, const QString& address, uint pid, const QString& name); + + DBusMgr* dbus() const + { + return m_dbus; + } + + /** + * @return The human readable client name, usually the process name + */ + QString name() const + { + return m_name; + } + + /** + * @return The unique DBus address of the client + */ + QString address() const + { + return m_address; + } + + /** + * @return The process id of the client + */ + uint pid() const + { + return m_pid; + } + + QSharedPointer + negotiateCipher(const QString& algorithm, const QVariant& input, QVariant& output, bool& incomplete); + + /** + * Check if the item is known in this client's auth list + */ + bool itemKnown(const QUuid& uuid) const; + + /** + * Check if client may access item identified by @a uuid. + */ + bool itemAuthorized(const QUuid& uuid) const; + + /** + * Check if client may access item identified by @a uuid, and also reset any once auth. + */ + bool itemAuthorizedResetOnce(const QUuid& uuid); + + /** + * Authorize client to access item identified by @a uuid. + */ + void setItemAuthorized(const QUuid& uuid, AuthDecision auth); + + /** + * Authorize client to access all items. + */ + void setAllAuthorized(bool authorized = true); + + /** + * Forget all previous authorization. + */ + void clearAuthorization(); + + /** + * Forcefully disconnect the client. + * Force close any remaining session, and cleanup negotiation states + */ + void disconnectDBus(); + + private: + QPointer m_dbus; + QString m_address; + + uint m_pid{0}; + QString m_name{}; + + bool m_authorizedAll{false}; + + QSet m_allowed{}; + QSet m_denied{}; + + QSet m_allowedOnce{}; + QSet m_deniedOnce{}; + }; + + using DBusClientPtr = QSharedPointer; +} // namespace FdoSecrets +Q_DECLARE_METATYPE(FdoSecrets::DBusClientPtr); + +#endif // KEEPASSXC_FDOSECRETS_DBUSCLIENT_H diff --git a/src/fdosecrets/dbus/DBusConstants.h b/src/fdosecrets/dbus/DBusConstants.h new file mode 100644 index 00000000..74c13d72 --- /dev/null +++ b/src/fdosecrets/dbus/DBusConstants.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSCONSTANTS_H +#define KEEPASSXC_FDOSECRETS_DBUSCONSTANTS_H + +#include + +static const auto DBUS_SERVICE_SECRET = QStringLiteral("org.freedesktop.secrets"); + +#define DBUS_INTERFACE_SECRET_SERVICE_LITERAL "org.freedesktop.Secret.Service" +#define DBUS_INTERFACE_SECRET_SESSION_LITERAL "org.freedesktop.Secret.Session" +#define DBUS_INTERFACE_SECRET_COLLECTION_LITERAL "org.freedesktop.Secret.Collection" +#define DBUS_INTERFACE_SECRET_ITEM_LITERAL "org.freedesktop.Secret.Item" +#define DBUS_INTERFACE_SECRET_PROMPT_LITERAL "org.freedesktop.Secret.Prompt" + +static const auto DBUS_INTERFACE_SECRET_SERVICE = QStringLiteral(DBUS_INTERFACE_SECRET_SERVICE_LITERAL); +static const auto DBUS_INTERFACE_SECRET_SESSION = QStringLiteral(DBUS_INTERFACE_SECRET_SESSION_LITERAL); +static const auto DBUS_INTERFACE_SECRET_COLLECTION = QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION_LITERAL); +static const auto DBUS_INTERFACE_SECRET_ITEM = QStringLiteral(DBUS_INTERFACE_SECRET_ITEM_LITERAL); +static const auto DBUS_INTERFACE_SECRET_PROMPT = QStringLiteral(DBUS_INTERFACE_SECRET_PROMPT_LITERAL); + +static const auto DBUS_ERROR_SECRET_NO_SESSION = QStringLiteral("org.freedesktop.Secret.Error.NoSession"); +static const auto DBUS_ERROR_SECRET_NO_SUCH_OBJECT = QStringLiteral("org.freedesktop.Secret.Error.NoSuchObject"); +static const auto DBUS_ERROR_SECRET_IS_LOCKED = QStringLiteral("org.freedesktop.Secret.Error.IsLocked"); + +static const auto DBUS_PATH_SECRETS = QStringLiteral("/org/freedesktop/secrets"); + +static const auto DBUS_PATH_TEMPLATE_ALIAS = QStringLiteral("%1/aliases/%2"); +static const auto DBUS_PATH_TEMPLATE_SESSION = QStringLiteral("%1/session/%2"); +static const auto DBUS_PATH_TEMPLATE_COLLECTION = QStringLiteral("%1/collection/%2"); +static const auto DBUS_PATH_TEMPLATE_ITEM = QStringLiteral("%1/%2"); +static const auto DBUS_PATH_TEMPLATE_PROMPT = QStringLiteral("%1/prompt/%2"); + +#endif // KEEPASSXC_FDOSECRETS_DBUSCONSTANTS_H diff --git a/src/fdosecrets/dbus/DBusDispatch.cpp b/src/fdosecrets/dbus/DBusDispatch.cpp new file mode 100644 index 00000000..eecce574 --- /dev/null +++ b/src/fdosecrets/dbus/DBusDispatch.cpp @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusMgr.h" + +#include "fdosecrets/dbus/DBusObject.h" +#include "fdosecrets/dbus/DBusTypes.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Service.h" + +#include "core/Global.h" + +#include +#include + +namespace FdoSecrets +{ + QString camelToPascal(const QString& camel) + { + if (camel.isEmpty()) { + return camel; + } + return camel.at(0).toUpper() + camel.mid(1); + } + + bool prepareInputParams(const QVector& inputTypes, + const QVariantList& args, + QVarLengthArray& params, + QVariantList& auxParams) + { + // prepare params + for (int count = 0; count != inputTypes.size(); ++count) { + const auto& id = inputTypes.at(count); + const auto& arg = args.at(count); + + if (arg.userType() == id) { + // shortcut for no conversion + params.append(const_cast(arg.constData())); + continue; + } + + // we need at least one conversion, allocate a slot in auxParams + auxParams.append(QVariant(id, nullptr)); + auto& out = auxParams.last(); + // first handle QDBusArgument to wire types + if (arg.userType() == qMetaTypeId()) { + auto wireId = typeToWireType(id).dbusTypeId; + out = QVariant(wireId, nullptr); + + const auto& in = arg.value(); + if (!QDBusMetaType::demarshall(in, wireId, out.data())) { + qDebug() << "Internal error: failed QDBusArgument conversion from" << arg << "to type" + << QMetaType::typeName(wireId) << wireId; + return false; + } + } else { + // make a copy to store the converted value + out = arg; + } + // other conversions are handled here + if (!out.convert(id)) { + qDebug() << "Internal error: failed conversion from" << arg << "to type" << QMetaType::typeName(id) + << id; + return false; + } + // good to go + params.append(const_cast(out.constData())); + } + return true; + } + + void DBusMgr::populateMethodCache(const QMetaObject& mo) + { + for (int i = mo.methodOffset(); i != mo.methodCount(); ++i) { + auto mm = mo.method(i); + + // only register public Q_INVOKABLE methods + if (mm.access() != QMetaMethod::Public || mm.methodType() != QMetaMethod::Method) { + continue; + } + if (mm.returnType() != qMetaTypeId()) { + continue; + } + + auto iface = mo.classInfo(mo.indexOfClassInfo("D-Bus Interface")).value(); + if (!iface) { + continue; + } + + // map from function name to dbus name + auto member = camelToPascal(mm.name()); + // also "remove" => "Delete" due to c++ keyword restriction + if (member == "Remove") { + member = QStringLiteral("Delete"); + } + auto cacheKey = QStringLiteral("%1.%2").arg(iface, member); + + // skip if we already have it + auto it = m_cachedMethods.find(cacheKey); + if (it != m_cachedMethods.end()) { + continue; + } + + MethodData md; + md.isProperty = mm.tag() && mm.tag() == QStringLiteral("DBUS_PROPERTY"); + md.slotIdx = mm.methodIndex(); + + bool valid = true; + // assumes output params (reference parameter) all follows input params + bool outputBegin = false; + for (const auto& paramType : mm.parameterTypes()) { + auto id = QMetaType::type(paramType); + + // handle the first optional calling client param + if (id == qMetaTypeId()) { + md.needsCallingClient = true; + continue; + } + + // handle output types + if (paramType.endsWith('&')) { + outputBegin = true; + id = QMetaType::type(paramType.left(paramType.length() - 1)); + md.outputTypes.append(id); + auto paramData = typeToWireType(id); + if (paramData.signature.isEmpty()) { + qDebug() << "Internal error: unhandled new output type for dbus signature" << paramType; + valid = false; + break; + } + md.outputTargetTypes.append(paramData.dbusTypeId); + continue; + } + + // handle input types + if (outputBegin) { + qDebug() << "Internal error: invalid method parameter order, no input parameter after output ones" + << mm.name(); + valid = false; + break; + } + auto sig = typeToWireType(id).signature; + if (sig.isEmpty()) { + qDebug() << "Internal error: unhandled new parameter type for dbus signature" << paramType; + valid = false; + break; + } + md.inputTypes.append(id); + md.signature += sig; + } + if (valid) { + m_cachedMethods.insert(cacheKey, md); + } + } + } + + bool DBusMgr::handleMessage(const QDBusMessage& message, const QDBusConnection&) + { + // save a mutable copy of the message, as we may modify it to unify property access + // and method call + RequestedMethod req{ + message.interface(), + message.member(), + message.signature(), + message.arguments(), + RequestType::Method, + }; + + if (req.interface == "org.freedesktop.DBus.Introspectable") { + // introspection can be handled by Qt, just return false + return false; + } else if (req.interface == "org.freedesktop.DBus.Properties") { + // but we need to handle properties ourselves like regular functions + if (!rewriteRequestForProperty(req)) { + // invalid message + qDebug() << "Invalid message" << message; + return false; + } + } + + // who's calling? + const auto& client = findClient(message.service()); + if (!client) { + // the client already died + return false; + } + + // activate the target object + return activateObject(client, message.path(), req, message); + } + + bool DBusMgr::rewriteRequestForProperty(RequestedMethod& req) + { + if (req.member == "Set" && req.signature == "ssv") { + // convert to normal method call: SetName + req.interface = req.args.at(0).toString(); + req.member = req.member + req.args.at(1).toString(); + // unwrap the QDBusVariant and expose the inner signature + auto arg = req.args.last().value().variant(); + req.args = {arg}; + if (arg.userType() == qMetaTypeId()) { + req.signature = arg.value().currentSignature(); + } else if (arg.userType() == QMetaType::QString) { + req.signature = "s"; + } else { + qDebug() << "Unhandled SetProperty value type" << QMetaType::typeName(arg.userType()) << arg.userType(); + return false; + } + } else if (req.member == "Get" && req.signature == "ss") { + // convert to normal method call: Name + req.interface = req.args.at(0).toString(); + req.member = req.args.at(1).toString(); + req.signature = ""; + req.args = {}; + req.type = RequestType::PropertyGet; + } else if (req.member == "GetAll" && req.signature == "s") { + // special handled in activateObject + req.interface = req.args.at(0).toString(); + req.member = ""; + req.signature = ""; + req.args = {}; + req.type = RequestType::PropertyGetAll; + } else { + return false; + } + return true; + } + + bool DBusMgr::activateObject(const DBusClientPtr& client, + const QString& path, + const RequestedMethod& req, + const QDBusMessage& msg) + { + auto obj = m_objects.value(path, nullptr); + if (!obj) { + qDebug() << "DBusMgr::handleMessage with unknown path" << msg; + return false; + } + Q_ASSERT_X(QThread::currentThread() == obj->thread(), + "QDBusConnection: internal threading error", + "function called for an object that is in another thread!!"); + + auto mo = obj->metaObject(); + // either interface matches, or interface is empty if req is property get all + QString interface = mo->classInfo(mo->indexOfClassInfo("D-Bus Interface")).value(); + if (req.interface != interface && !(req.type == RequestType::PropertyGetAll && req.interface.isEmpty())) { + qDebug() << "DBusMgr::handleMessage with mismatch interface" << msg; + return false; + } + + // special handle of property getall + if (req.type == RequestType::PropertyGetAll) { + return objectPropertyGetAll(client, obj, interface, msg); + } + + // find the slot to call + auto cacheKey = QStringLiteral("%1.%2").arg(req.interface, req.member); + auto it = m_cachedMethods.find(cacheKey); + if (it == m_cachedMethods.end()) { + qDebug() << "DBusMgr::handleMessage with nonexisting method" << cacheKey; + return false; + } + + // requested signature is verified by Qt to match the content of arguments, + // but this list of arguments itself is untrusted + if (it->signature != req.signature || it->inputTypes.size() != req.args.size()) { + qDebug() << "Message signature does not match, expected" << it->signature << it->inputTypes.size() << "got" + << req.signature << req.args.size(); + return false; + } + + DBusResult ret; + QVariantList outputArgs; + if (!deliverMethod(client, obj, *it, req.args, ret, outputArgs)) { + qDebug() << "Failed to deliver method" << msg; + return sendDBus(msg.createErrorReply(QDBusError::InternalError, tr("Failed to deliver message"))); + } + + if (!ret.ok()) { + return sendDBus(msg.createErrorReply(ret, "")); + } + if (req.type == RequestType::PropertyGet) { + // property get need the reply wrapped in QDBusVariant + outputArgs[0] = QVariant::fromValue(QDBusVariant(outputArgs.first())); + } + return sendDBus(msg.createReply(outputArgs)); + } + + bool DBusMgr::objectPropertyGetAll(const DBusClientPtr& client, + DBusObject* obj, + const QString& interface, + const QDBusMessage& msg) + { + QVariantMap result; + + // prefix match the cacheKey + auto prefix = interface + "."; + for (auto it = m_cachedMethods.constBegin(); it != m_cachedMethods.constEnd(); ++it) { + if (!it.key().startsWith(prefix)) { + continue; + } + if (!it.value().isProperty) { + continue; + } + auto name = it.key().mid(prefix.size()); + + DBusResult ret; + QVariantList outputArgs; + if (!deliverMethod(client, obj, it.value(), {}, ret, outputArgs)) { + // ignore any error per spec + continue; + } + if (ret.err()) { + // ignore any error per spec + continue; + } + Q_ASSERT(outputArgs.size() == 1); + + result.insert(name, outputArgs.first()); + } + + return sendDBus(msg.createReply(QVariantList{result})); + } + + bool DBusMgr::deliverMethod(const DBusClientPtr& client, + DBusObject* obj, + const MethodData& method, + const QVariantList& args, + DBusResult& ret, + QVariantList& outputArgs) + { + QVarLengthArray params; + QVariantList auxParams; + + // the first one is for return type + params.append(&ret); + + if (method.needsCallingClient) { + auxParams.append(QVariant::fromValue(client)); + params.append(const_cast(auxParams.last().constData())); + } + + // prepare input + if (!prepareInputParams(method.inputTypes, args, params, auxParams)) { + qDebug() << "Failed to prepare input params"; + return false; + } + + // prepare output args + outputArgs.reserve(outputArgs.size() + method.outputTypes.size()); + for (const auto& outputType : asConst(method.outputTypes)) { + outputArgs.append(QVariant(outputType, nullptr)); + params.append(const_cast(outputArgs.last().constData())); + } + + // call it + bool fail = obj->qt_metacall(QMetaObject::InvokeMetaMethod, method.slotIdx, params.data()) >= 0; + if (fail) { + // generate internal error + qWarning() << "Internal error: Failed to deliver message"; + return false; + } + + if (!ret.ok()) { + // error reply + return true; + } + + // output args need to be converted before they can be directly sent out: + for (int i = 0; i != outputArgs.size(); ++i) { + auto& outputArg = outputArgs[i]; + if (!outputArg.convert(method.outputTargetTypes.at(i))) { + qWarning() << "Internal error: Failed to convert message output to type" + << method.outputTargetTypes.at(i); + return false; + } + } + + return true; + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusMgr.cpp b/src/fdosecrets/dbus/DBusMgr.cpp new file mode 100644 index 00000000..cd44ce2f --- /dev/null +++ b/src/fdosecrets/dbus/DBusMgr.cpp @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusMgr.h" + +#include "fdosecrets/dbus/DBusConstants.h" +#include "fdosecrets/dbus/DBusTypes.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" + +#include "core/Entry.h" +#include "core/Tools.h" + +#include +#include +#include + +namespace FdoSecrets +{ + static const auto IntrospectionService = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionCollection = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionItem = R"xml( + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionSession = R"xml( + + + + +)xml"; + + static const auto IntrospectionPrompt = R"xml( + + + + + + + + + + + +)xml"; + + DBusMgr::DBusMgr() + : m_conn(QDBusConnection::sessionBus()) + { + // remove client when it disappears on the bus + m_watcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(&m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, &DBusMgr::dbusServiceUnregistered); + m_watcher.setConnection(m_conn); + } + + void DBusMgr::populateMethodCache() + { + // these are the methods we expose on DBus + populateMethodCache(Service::staticMetaObject); + populateMethodCache(Collection::staticMetaObject); + populateMethodCache(Item::staticMetaObject); + populateMethodCache(PromptBase::staticMetaObject); + populateMethodCache(Session::staticMetaObject); + } + + DBusMgr::~DBusMgr() = default; + + void DBusMgr::overrideClient(const DBusClientPtr& fake) + { + m_overrideClient = fake; + } + + QList DBusMgr::clients() const + { + return m_clients.values(); + } + + bool DBusMgr::serviceInfo(const QString& addr, ProcessInfo& info) const + { + auto pid = m_conn.interface()->servicePid(addr); + if (!pid.isValid()) { + return false; + } + info.pid = pid.value(); + // The /proc/pid/exe link is more reliable than /proc/pid/cmdline + // It's still weak and if the application does a prctl(PR_SET_DUMPABLE, 0) this link cannot be accessed. + QFileInfo proc(QStringLiteral("/proc/%1/exe").arg(pid.value())); + info.exePath = proc.canonicalFilePath(); + + return true; + } + + bool DBusMgr::sendDBusSignal(const QString& path, + const QString& interface, + const QString& name, + const QVariantList& arguments) + { + auto msg = QDBusMessage::createSignal(path, interface, name); + msg.setArguments(arguments); + return sendDBus(msg); + } + + bool DBusMgr::sendDBus(const QDBusMessage& reply) + { + bool ok = m_conn.send(reply); + if (!ok) { + qDebug() << "Failed to send on DBus:" << reply; + emit error(tr("Failed to send reply on DBus")); + } + return ok; + } + + // `this` object is registered at multiple paths: + // /org/freedesktop/secrets + // /org/freedesktop/secrets/collection/xxx + // /org/freedesktop/secrets/collection/xxx/yyy + // /org/freedesktop/secrets/aliases/xxx + // /org/freedesktop/secrets/session/xxx + // /org/freedesktop/secrets/prompt/xxx + // + // The path validation is left to Qt, this method only do the minimum + // required to differentiate the paths. + DBusMgr::ParsedPath DBusMgr::parsePath(const QString& path) + { + Q_ASSERT(path.startsWith('/')); + Q_ASSERT(path == "/" || !path.endsWith('/')); + + static const QString DBusPathSecrets = DBUS_PATH_SECRETS; + + if (!path.startsWith(DBusPathSecrets)) { + return ParsedPath{}; + } + auto parts = path.mid(DBusPathSecrets.size()).split('/'); + // the first part is always empty + if (parts.isEmpty() || parts.first() != "") { + return ParsedPath{}; + } + parts.takeFirst(); + + if (parts.isEmpty()) { + return ParsedPath{PathType::Service}; + } else if (parts.size() == 2) { + if (parts.at(0) == "collection") { + return ParsedPath{PathType::Collection, parts.at(1)}; + } else if (parts.at(0) == "aliases") { + return ParsedPath{PathType::Aliases, parts.at(1)}; + } else if (parts.at(0) == "prompt") { + return ParsedPath{PathType::Prompt, parts.at(1)}; + } else if (parts.at(0) == "session") { + return ParsedPath{PathType::Session, parts.at(1)}; + } + } else if (parts.size() == 3) { + if (parts.at(0) == "collection") { + return ParsedPath{PathType::Item, parts.at(2), parts.at(1)}; + } + } + return ParsedPath{}; + } + + QString DBusMgr::introspect(const QString& path) const + { + auto parsed = parsePath(path); + switch (parsed.type) { + case PathType::Service: + return IntrospectionService; + case PathType::Collection: + case PathType::Aliases: + return IntrospectionCollection; + case PathType::Prompt: + return IntrospectionPrompt; + case PathType::Session: + return IntrospectionSession; + case PathType::Item: + return IntrospectionItem; + case PathType::Unknown: + default: + return ""; + } + } + + bool DBusMgr::serviceOccupied() const + { + auto reply = m_conn.interface()->isServiceRegistered(DBUS_SERVICE_SECRET); + if (!reply.isValid()) { + return false; + } + if (reply.value()) { + auto pid = m_conn.interface()->servicePid(DBUS_SERVICE_SECRET); + if (pid.isValid() && pid.value() != qApp->applicationPid()) { + return true; + } + } + return false; + } + + QString DBusMgr::reportExistingService() const + { + auto pidStr = tr("Unknown", "Unknown PID"); + auto exeStr = tr("Unknown", "Unknown executable path"); + + ProcessInfo info{}; + if (serviceInfo(DBUS_SERVICE_SECRET, info)) { + pidStr = QString::number(info.pid); + if (!info.exePath.isEmpty()) { + exeStr = info.exePath; + } + } + + auto otherService = tr("PID: %1, Executable: %2", "PID: 1234, Executable: /path/to/exe") + .arg(pidStr, exeStr.toHtmlEscaped()); + return tr("Another secret service is running (%1).
" + "Please stop/remove it before re-enabling the Secret Service Integration.") + .arg(otherService); + } + + bool DBusMgr::registerObject(const QString& path, DBusObject* obj, bool primary) + { + if (!m_conn.registerVirtualObject(path, this)) { + qDebug() << "failed to register" << obj << "at" << path; + return false; + } + connect(obj, &DBusObject::destroyed, this, &DBusMgr::unregisterObject); + m_objects.insert(path, obj); + if (primary) { + obj->setObjectPath(path); + } + return true; + } + + bool DBusMgr::registerObject(Service* service) + { + if (!m_conn.registerService(DBUS_SERVICE_SECRET)) { + const auto existing = reportExistingService(); + qDebug() << "Failed to register DBus service at " << DBUS_SERVICE_SECRET; + qDebug() << existing; + emit error(tr("Failed to register DBus service at %1.
").arg(DBUS_SERVICE_SECRET) + existing); + return false; + } + connect(service, &DBusObject::destroyed, this, [this]() { m_conn.unregisterService(DBUS_SERVICE_SECRET); }); + + if (!registerObject(DBUS_PATH_SECRETS, service)) { + qDebug() << "Failed to register service on DBus at path" << DBUS_PATH_SECRETS; + emit error(tr("Failed to register service on DBus at path '%1'").arg(DBUS_PATH_SECRETS)); + return false; + } + + connect(service, &Service::collectionCreated, this, &DBusMgr::emitCollectionCreated); + connect(service, &Service::collectionChanged, this, &DBusMgr::emitCollectionChanged); + connect(service, &Service::collectionDeleted, this, &DBusMgr::emitCollectionDeleted); + + return true; + } + + bool DBusMgr::registerObject(Collection* coll) + { + auto name = encodePath(coll->name()); + auto path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name); + if (!registerObject(path, coll)) { + // try again with a suffix + name.append(QString("_%1").arg(Tools::uuidToHex(QUuid::createUuid()).left(4))); + path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name); + + if (!registerObject(path, coll)) { + qDebug() << "Failed to register database on DBus under name" << name; + emit error(tr("Failed to register database on DBus under the name '%1'").arg(name)); + return false; + } + } + + connect(coll, &Collection::itemCreated, this, &DBusMgr::emitItemCreated); + connect(coll, &Collection::itemChanged, this, &DBusMgr::emitItemChanged); + connect(coll, &Collection::itemDeleted, this, &DBusMgr::emitItemDeleted); + + return true; + } + + bool DBusMgr::registerObject(Session* sess) + { + auto path = DBUS_PATH_TEMPLATE_SESSION.arg(DBUS_PATH_SECRETS, sess->id()); + if (!registerObject(path, sess)) { + emit error(tr("Failed to register session on DBus at path '%1'").arg(path)); + return false; + } + return true; + } + + bool DBusMgr::registerObject(Item* item) + { + auto path = DBUS_PATH_TEMPLATE_ITEM.arg(item->collection()->objectPath().path(), item->backend()->uuidToHex()); + if (!registerObject(path, item)) { + emit error(tr("Failed to register item on DBus at path '%1'").arg(path)); + return false; + } + return true; + } + + bool DBusMgr::registerObject(PromptBase* prompt) + { + auto path = DBUS_PATH_TEMPLATE_PROMPT.arg(DBUS_PATH_SECRETS, Tools::uuidToHex(QUuid::createUuid())); + if (!registerObject(path, prompt)) { + emit error(tr("Failed to register prompt object on DBus at path '%1'").arg(path)); + return false; + } + + connect(prompt, &PromptBase::completed, this, &DBusMgr::emitPromptCompleted); + + return true; + } + + void DBusMgr::unregisterObject(DBusObject* obj) + { + auto count = m_objects.remove(obj->objectPath().path()); + if (count > 0) { + m_conn.unregisterObject(obj->objectPath().path()); + obj->setObjectPath("/"); + } + } + + bool DBusMgr::registerAlias(Collection* coll, const QString& alias) + { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + if (!registerObject(path, coll, false)) { + qDebug() << "Failed to register database on DBus under alias" << alias; + // usually this is reported back directly on dbus, so no need to show in UI + return false; + } + // alias signals are handled together with collections' primary path in emitCollection* + // but we need to handle object destroy here + connect(coll, &DBusObject::destroyed, this, [this, alias]() { unregisterAlias(alias); }); + return true; + } + + void DBusMgr::unregisterAlias(const QString& alias) + { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + // DBusMgr::unregisterObject only handles primary path + m_objects.remove(path); + m_conn.unregisterObject(path); + } + + void DBusMgr::emitCollectionCreated(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionCreated"), args); + } + + void DBusMgr::emitCollectionChanged(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, "CollectionChanged", args); + } + + void DBusMgr::emitCollectionDeleted(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionDeleted"), args); + } + + void DBusMgr::emitItemCreated(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args); + } + } + + void DBusMgr::emitItemChanged(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args); + } + } + + void DBusMgr::emitItemDeleted(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args); + } + } + + void DBusMgr::emitPromptCompleted(bool dismissed, QVariant result) + { + auto prompt = qobject_cast(sender()); + if (!prompt) { + qDebug() << "Wrong sender in emitPromptCompleted"; + return; + } + + // make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it. + if (!result.isValid()) { + result = QString{}; + } + + QVariantList args; + args += QVariant::fromValue(dismissed); + args += QVariant::fromValue(QDBusVariant(result)); + sendDBusSignal(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT, QStringLiteral("Completed"), args); + } + + DBusClientPtr DBusMgr::findClient(const QString& addr) + { + if (m_overrideClient) { + return m_overrideClient; + } + + auto it = m_clients.find(addr); + if (it == m_clients.end()) { + auto client = createClient(addr); + if (!client) { + return {}; + } + it = m_clients.insert(addr, client); + } + // double check the client + ProcessInfo info{}; + if (!serviceInfo(addr, info) || info.pid != it.value()->pid()) { + dbusServiceUnregistered(addr); + return {}; + } + return it.value(); + } + + DBusClientPtr DBusMgr::createClient(const QString& addr) + { + ProcessInfo info{}; + if (!serviceInfo(addr, info)) { + return {}; + } + + auto client = DBusClientPtr(new DBusClient(this, addr, info.pid, info.exePath.isEmpty() ? addr : info.exePath)); + + emit clientConnected(client); + m_watcher.addWatchedService(addr); + + return client; + } + + void DBusMgr::removeClient(DBusClient* client) + { + if (!client) { + return; + } + + auto it = m_clients.find(client->address()); + if (it == m_clients.end()) { + return; + } + + emit clientDisconnected(*it); + m_clients.erase(it); + } + + void DBusMgr::dbusServiceUnregistered(const QString& service) + { + auto removed = m_watcher.removeWatchedService(service); + if (!removed) { + qDebug("FdoSecrets: Failed to remove service watcher"); + } + + auto it = m_clients.find(service); + if (it == m_clients.end()) { + return; + } + auto client = it.value(); + + client->disconnectDBus(); + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusMgr.h b/src/fdosecrets/dbus/DBusMgr.h new file mode 100644 index 00000000..ce4a88fc --- /dev/null +++ b/src/fdosecrets/dbus/DBusMgr.h @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSMGR_H +#define KEEPASSXC_FDOSECRETS_DBUSMGR_H + +#include "fdosecrets/dbus/DBusClient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class TestFdoSecrets; + +namespace FdoSecrets +{ + class Collection; + class Service; + class PromptBase; + class Session; + class Item; + class DBusObject; + class DBusResult; + + /** + * DBusMgr takes care of the interaction between dbus and business logic objects (DBusObject). It handles the + * following + * - Registering/unregistering service name + * - Registering/unregistering paths + * - Relay signals from DBusObject to dbus + * - Manage per-client states, mapping from dbus caller address to Client + * - Deliver method calls from dbus to DBusObject + * + * Special note in implementation of method delivery: + * There are two sets of vocabulary classes in use for method delivery. + * The Qt DBus system uses QDBusVariant/QDBusObjectPath and other primitive types in QDBusMessage::arguments(), + * i.e. the on-the-wire types. + * The DBusObject invokable methods uses QVariant/DBusObject* and other primitive types in parameters (parameter + * types). FdoSecrets::typeToWireType establishes the mapping from parameter types to on-the-wire types. The + * conversion between types is done with the help of QMetaType convert. + * + * The method delivery sequence: + * - DBusMgr::handleMessage unifies method call and property access into the same form + * - DBusMgr::activateObject finds the target object and calls the method by doing the following + * * check the object exists and the interface matches + * * find the cached method information MethodData + * * DBusMgr::prepareInputParams check and convert input arguments in QDBusMessage::arguments() to types expected + * by DBusObject + * * prepare output argument storage + * * call the method + * * convert types to what Qt DBus expects + * + * The MethodData is pre-computed using Qt meta object system by finding methods with signature matching a certain + * pattern: + * Q_INVOKABLE DBusResult methodName(const DBusClientPtr& client, + * const X& input1, + * const Y& input2, + * Z& output1, + * ZZ& output2) + * Note that the first parameter of client is optional. + */ + class DBusMgr : public QDBusVirtualObject + { + Q_OBJECT + public: + explicit DBusMgr(); + + /** + * @brief Must be called after all dbus types are registered + */ + void populateMethodCache(); + + ~DBusMgr() override; + + QString introspect(const QString& path) const override; + bool handleMessage(const QDBusMessage& message, const QDBusConnection& connection) override; + + /** + * @return current connected clients + */ + QList clients() const; + + /** + * @return whether the org.freedesktop.secrets service is owned by others + */ + bool serviceOccupied() const; + + /** + * Check the running secret service and return info about it + * @return html string suitable to be shown in the UI + */ + QString reportExistingService() const; + + // expose on dbus and handle signals + bool registerObject(Service* service); + bool registerObject(Collection* coll); + bool registerObject(Session* sess); + bool registerObject(Item* item); + bool registerObject(PromptBase* prompt); + + void unregisterObject(DBusObject* obj); + + // and the signals are handled together with collection's primary path + bool registerAlias(Collection* coll, const QString& alias); + void unregisterAlias(const QString& alias); + + /** + * Return the object path of the pointed DBusObject, or "/" if the pointer is null + * @tparam T + * @param object + * @return + */ + template static QDBusObjectPath objectPathSafe(T* object) + { + if (object) { + return object->objectPath(); + } + return QDBusObjectPath(QStringLiteral("/")); + } + template static QDBusObjectPath objectPathSafe(QPointer object) + { + return objectPathSafe(object.data()); + } + static QDBusObjectPath objectPathSafe(std::nullptr_t) + { + return QDBusObjectPath(QStringLiteral("/")); + } + + /** + * Convert a list of DBusObjects to object path + * @tparam T + * @param objects + * @return + */ + template static QList objectsToPath(QList objects) + { + QList res; + res.reserve(objects.size()); + for (auto object : objects) { + res.append(objectPathSafe(object)); + } + return res; + } + + /** + * Convert an object path to a pointer of the object + * @tparam T + * @param path + * @return the pointer of the object, or nullptr if path is "/" + */ + template T* pathToObject(const QDBusObjectPath& path) const + { + if (path.path() == QStringLiteral("/")) { + return nullptr; + } + auto obj = qobject_cast(m_objects.value(path.path(), nullptr)); + if (!obj) { + qDebug() << "object not found at path" << path.path(); + qDebug() << m_objects; + } + return obj; + } + + /** + * Convert a list of object paths to a list of objects. + * "/" paths (i.e. nullptrs) will be skipped in the resulting list + * @tparam T + * @param paths + * @return + */ + template QList pathsToObject(const QList& paths) const + { + QList res; + res.reserve(paths.size()); + for (const auto& path : paths) { + auto object = pathToObject(path); + if (object) { + res.append(object); + } + } + return res; + } + + // Force client to be a specific object, used for testing + void overrideClient(const DBusClientPtr& fake); + + signals: + void clientConnected(const DBusClientPtr& client); + void clientDisconnected(const DBusClientPtr& client); + void error(const QString& msg); + + private slots: + void emitCollectionCreated(Collection* coll); + void emitCollectionChanged(Collection* coll); + void emitCollectionDeleted(Collection* coll); + void emitItemCreated(Item* item); + void emitItemChanged(Item* item); + void emitItemDeleted(Item* item); + void emitPromptCompleted(bool dismissed, QVariant result); + + void dbusServiceUnregistered(const QString& service); + + private: + QDBusConnection m_conn; + + struct ProcessInfo + { + uint pid; + QString exePath; + }; + bool serviceInfo(const QString& addr, ProcessInfo& info) const; + + bool sendDBusSignal(const QString& path, + const QString& interface, + const QString& name, + const QVariantList& arguments); + bool sendDBus(const QDBusMessage& reply); + + // object path registration + QHash> m_objects{}; + enum class PathType + { + Service, + Collection, + Aliases, + Prompt, + Session, + Item, + Unknown, + }; + struct ParsedPath + { + PathType type; + QString id; + // only used when type == Item + QString parentId; + explicit ParsedPath(PathType type = PathType::Unknown, QString id = "", QString parentId = "") + : type(type) + , id(std::move(id)) + , parentId(std::move(parentId)) + { + } + }; + static ParsedPath parsePath(const QString& path); + bool registerObject(const QString& path, DBusObject* obj, bool primary = true); + + // method dispatching + struct MethodData + { + int slotIdx{-1}; + QByteArray signature{}; + QVector inputTypes{}; + QVector outputTypes{}; + QVector outputTargetTypes{}; + bool isProperty{false}; + bool needsCallingClient{false}; + }; + QHash m_cachedMethods{}; + void populateMethodCache(const QMetaObject& mo); + + enum class RequestType + { + Method, + PropertyGet, + PropertyGetAll, + }; + struct RequestedMethod + { + QString interface; + QString member; + QString signature; + QVariantList args; + RequestType type; + }; + static bool rewriteRequestForProperty(RequestedMethod& req); + bool activateObject(const DBusClientPtr& client, + const QString& path, + const RequestedMethod& req, + const QDBusMessage& msg); + bool objectPropertyGetAll(const DBusClientPtr& client, + DBusObject* obj, + const QString& interface, + const QDBusMessage& msg); + static bool deliverMethod(const DBusClientPtr& client, + DBusObject* obj, + const MethodData& method, + const QVariantList& args, + DBusResult& ret, + QVariantList& outputArgs); + + // client management + friend class DBusClient; + + DBusClientPtr findClient(const QString& addr); + DBusClientPtr createClient(const QString& addr); + + /** + * @brief This gets called from DBusClient::disconnectDBus + * @param client + */ + void removeClient(DBusClient* client); + + QDBusServiceWatcher m_watcher{}; + // mapping from the unique dbus peer address to client object + QHash m_clients{}; + + DBusClientPtr m_overrideClient; + + friend class ::TestFdoSecrets; + }; +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_DBUSMGR_H diff --git a/src/fdosecrets/objects/DBusObject.cpp b/src/fdosecrets/dbus/DBusObject.cpp similarity index 69% rename from src/fdosecrets/objects/DBusObject.cpp rename to src/fdosecrets/dbus/DBusObject.cpp index fb7533c1..63a8df60 100644 --- a/src/fdosecrets/objects/DBusObject.cpp +++ b/src/fdosecrets/dbus/DBusObject.cpp @@ -19,37 +19,32 @@ #include #include -#include #include -#include namespace FdoSecrets { DBusObject::DBusObject(DBusObject* parent) : QObject(parent) - , m_dbusAdaptor(nullptr) + , m_dbus(parent->dbus()) { } - bool DBusObject::registerWithPath(const QString& path, bool primary) + DBusObject::DBusObject(QSharedPointer dbus) + : QObject(nullptr) + , m_objectPath("/") + , m_dbus(std::move(dbus)) { - if (primary) { - m_objectPath.setPath(path); - } - - return QDBusConnection::sessionBus().registerObject(path, this); } - QString DBusObject::callingPeerName() const + DBusObject::~DBusObject() { - auto pid = callingPeerPid(); - QFile proc(QStringLiteral("/proc/%1/comm").arg(pid)); - if (!proc.open(QFile::ReadOnly)) { - return callingPeer(); - } - QTextStream stream(&proc); - return stream.readAll().trimmed(); + emit destroyed(this); + } + + void DBusObject::setObjectPath(const QString& path) + { + m_objectPath.setPath(path); } QString encodePath(const QString& value) diff --git a/src/fdosecrets/dbus/DBusObject.h b/src/fdosecrets/dbus/DBusObject.h new file mode 100644 index 00000000..d1177890 --- /dev/null +++ b/src/fdosecrets/dbus/DBusObject.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSOBJECT_H +#define KEEPASSXC_FDOSECRETS_DBUSOBJECT_H + +#include "DBusConstants.h" +#include "DBusMgr.h" +#include "DBusTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_MOC_RUN +// define the tag text as empty, so the compiler doesn't see it +#define DBUS_PROPERTY +#endif // #ifndef Q_MOC_RUN + +namespace FdoSecrets +{ + class Service; + + /** + * @brief A common base class for all dbus-exposed objects. + */ + class DBusObject : public QObject + { + Q_OBJECT + public: + ~DBusObject() override; + + const QDBusObjectPath& objectPath() const + { + return m_objectPath; + } + + const QSharedPointer& dbus() const + { + return m_dbus; + } + + signals: + /** + * @brief Necessary because by the time QObject::destroyed is emitted, + * we already lost any info in DBusObject + */ + void destroyed(DBusObject* self); + + protected: + explicit DBusObject(DBusObject* parent); + explicit DBusObject(QSharedPointer dbus); + + private: + friend class DBusMgr; + void setObjectPath(const QString& path); + + QDBusObjectPath m_objectPath; + QSharedPointer m_dbus; + }; + + /** + * @brief A dbus error or not + */ + class DBusResult : public QString + { + public: + DBusResult() = default; + explicit DBusResult(QString error) + : QString(std::move(error)) + { + } + + // Implicitly convert from QDBusError + DBusResult(QDBusError::ErrorType error) // NOLINT(google-explicit-constructor) + : QString(QDBusError::errorString(error)) + { + } + + bool ok() const + { + return isEmpty(); + } + bool err() const + { + return !isEmpty(); + } + void okOrDie() const + { + Q_ASSERT(ok()); + } + }; + + /** + * Encode the string value to a DBus object path safe representation, + * using a schema similar to URI encoding, but with percentage(%) replaced with + * underscore(_). All characters except [A-Za-z0-9] are encoded. For non-ascii + * characters, UTF-8 encoding is first applied and each of the resulting byte + * value is encoded. + * @param value + * @return encoded string + */ + QString encodePath(const QString& value); + +} // namespace FdoSecrets + +Q_DECLARE_METATYPE(FdoSecrets::DBusResult); + +#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H diff --git a/src/fdosecrets/dbus/DBusTypes.cpp b/src/fdosecrets/dbus/DBusTypes.cpp new file mode 100644 index 00000000..715c9523 --- /dev/null +++ b/src/fdosecrets/dbus/DBusTypes.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2019 Aetf + * Copyright 2010, Michael Leupold + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusTypes.h" + +#include "fdosecrets/dbus/DBusMgr.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" + +#include + +namespace FdoSecrets +{ + bool inherits(const QMetaObject* derived, const QMetaObject* base) + { + for (auto super = derived; super; super = super->superClass()) { + if (super == base) { + return true; + } + } + return false; + } + + template void registerConverter(const QWeakPointer& weak) + { + // from parameter type to on-the-wire type + QMetaType::registerConverter([](const T* obj) { return DBusMgr::objectPathSafe(obj); }); + QMetaType::registerConverter, QList>( + [](const QList objs) { return DBusMgr::objectsToPath(objs); }); + + // the opposite + QMetaType::registerConverter([weak](const QDBusObjectPath& path) -> T* { + if (auto dbus = weak.lock()) { + return dbus->pathToObject(path); + } + qDebug() << "No DBusMgr when looking up path" << path.path(); + return nullptr; + }); + QMetaType::registerConverter, QList>([weak](const QList& paths) { + if (auto dbus = weak.lock()) { + return dbus->pathsToObject(paths); + } + qDebug() << "No DBusMgr when looking up paths"; + return QList{}; + }); + } + + void registerDBusTypes(const QSharedPointer& dbus) + { + // On the wire types: + // - various primary types + // - QDBusVariant + // - wire::Secret + // - wire::ObjectPathSecretMap + // - QDBusObjectPath + // - QList + + // Parameter types: + // - various primary types + // - QVariant + // - Secret + // - ObjectSecretMap + // - DBusObject* (and derived classes) + // - QList + + // NOTE: when registering, in additional to the class' fully qualified name, + // the partial-namespace/non-namespace name should also be registered as alias + // otherwise all those usages in Q_INVOKABLE methods without FQN won't be included + // in the meta type system. +#define REG_METATYPE(type) \ + qRegisterMetaType(); \ + qRegisterMetaType(#type) + + // register on-the-wire types + // Qt container types for builtin types don't need registration + REG_METATYPE(wire::Secret); + REG_METATYPE(wire::StringStringMap); + REG_METATYPE(wire::ObjectPathSecretMap); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + // register parameter types + REG_METATYPE(Secret); + REG_METATYPE(StringStringMap); + REG_METATYPE(ItemSecretMap); + REG_METATYPE(DBusResult); + REG_METATYPE(DBusClientPtr); + +#define REG_DBUS_OBJ(name) \ + REG_METATYPE(name*); \ + REG_METATYPE(QList) + REG_DBUS_OBJ(DBusObject); + REG_DBUS_OBJ(Service); + REG_DBUS_OBJ(Collection); + REG_DBUS_OBJ(Item); + REG_DBUS_OBJ(Session); + REG_DBUS_OBJ(PromptBase); +#undef REG_DBUS_OBJ + +#undef REG_METATYPE + + QWeakPointer weak = dbus; + // register converter between on-the-wire types and parameter types + // some pairs are missing because that particular direction isn't used + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + + QMetaType::registerConverter( + [weak](const wire::Secret& from) { return from.unmarshal(weak); }); + QMetaType::registerConverter(&Secret::marshal); + + QMetaType::registerConverter([](const ItemSecretMap& map) { + wire::ObjectPathSecretMap ret; + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + ret.insert(it.key()->objectPath(), it.value().marshal()); + } + return ret; + }); + + QMetaType::registerConverter([](const QDBusVariant& obj) { return obj.variant(); }); + QMetaType::registerConverter([](const QVariant& obj) { return QDBusVariant(obj); }); + + // structural types are received as QDBusArgument, + // top level QDBusArgument in method parameters are directly handled + // in prepareInputParams. + // But in Collection::createItem, we need to convert a inner QDBusArgument to StringStringMap + QMetaType::registerConverter([](const QDBusArgument& arg) { + if (arg.currentSignature() != "a{ss}") { + return StringStringMap{}; + } + // QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const. + // we don't want to modify the instance (arg) stored in the qvariant so we create a copy + const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization) + return qdbus_cast(copy); + }); + } + + ParamData typeToWireType(int id) + { + switch (id) { + case QMetaType::QString: + return {QByteArrayLiteral("s"), QMetaType::QString}; + case QMetaType::QVariant: + return {QByteArrayLiteral("v"), qMetaTypeId()}; + case QMetaType::QVariantMap: + return {QByteArrayLiteral("a{sv}"), QMetaType::QVariantMap}; + case QMetaType::Bool: + return {QByteArrayLiteral("b"), QMetaType::Bool}; + case QMetaType::ULongLong: + return {QByteArrayLiteral("t"), QMetaType::ULongLong}; + default: + break; + } + if (id == qMetaTypeId()) { + return {QByteArrayLiteral("a{ss}"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("a{o(oayays)}"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("(oayays)"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("o"), qMetaTypeId()}; + } else if (id == qMetaTypeId>()) { + return {QByteArrayLiteral("ao"), qMetaTypeId>()}; + } + + QMetaType mt(id); + if (!mt.isValid()) { + return {}; + } + if (QByteArray(QMetaType::typeName(id)).startsWith("QList")) { + // QList + return {QByteArrayLiteral("ao"), qMetaTypeId>()}; + } + if (!inherits(mt.metaObject(), &DBusObject::staticMetaObject)) { + return {}; + } + // DBusObjects + return {QByteArrayLiteral("o"), qMetaTypeId()}; + } + + ::FdoSecrets::Secret wire::Secret::unmarshal(const QWeakPointer& weak) const + { + if (auto dbus = weak.lock()) { + return {dbus->pathToObject(session), parameters, value, contentType}; + } + qDebug() << "No DBusMgr when converting wire::Secret"; + return {nullptr, parameters, value, contentType}; + } + + wire::Secret Secret::marshal() const + { + return {DBusMgr::objectPathSafe(session), parameters, value, contentType}; + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusTypes.h b/src/fdosecrets/dbus/DBusTypes.h new file mode 100644 index 00000000..01171e53 --- /dev/null +++ b/src/fdosecrets/dbus/DBusTypes.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 Aetf + * Copyright 2010, Michael Leupold + * Copyright 2010-2011, Valentin Rusu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSTYPES_H +#define KEEPASSXC_FDOSECRETS_DBUSTYPES_H + +#include +#include +#include +#include + +namespace FdoSecrets +{ + struct Secret; + class DBusMgr; + + // types used directly in Qt DBus system + namespace wire + { + struct Secret + { + QDBusObjectPath session; + QByteArray parameters; + QByteArray value; + QString contentType; + + ::FdoSecrets::Secret unmarshal(const QWeakPointer& weak) const; + }; + + inline QDBusArgument& operator<<(QDBusArgument& argument, const Secret& secret) + { + argument.beginStructure(); + argument << secret.session << secret.parameters << secret.value << secret.contentType; + argument.endStructure(); + return argument; + } + + inline const QDBusArgument& operator>>(const QDBusArgument& argument, Secret& secret) + { + argument.beginStructure(); + argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType; + argument.endStructure(); + return argument; + } + + using StringStringMap = QMap; + using ObjectPathSecretMap = QMap; + } // namespace wire + + // types used in method parameters + class Session; + class Item; + struct Secret + { + const Session* session; + QByteArray parameters; + QByteArray value; + QString contentType; + + wire::Secret marshal() const; + }; + using wire::StringStringMap; + using ItemSecretMap = QHash; + + /** + * Register the types needed for the fd.o Secrets D-Bus interface. + */ + void registerDBusTypes(const QSharedPointer& dbus); + + struct ParamData + { + QByteArray signature; + int dbusTypeId; + }; + + /** + * @brief Convert parameter type to on-the-wire type and associated dbus signature. + * This is NOT a generic version, and only handles types used in org.freedesktop.secrets + * @param id + * @return ParamData + */ + ParamData typeToWireType(int id); +} // namespace FdoSecrets + +Q_DECLARE_METATYPE(FdoSecrets::wire::Secret) +Q_DECLARE_METATYPE(FdoSecrets::wire::StringStringMap); +Q_DECLARE_METATYPE(FdoSecrets::wire::ObjectPathSecretMap); + +Q_DECLARE_METATYPE(FdoSecrets::Secret) + +#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp index 0f856d87..f4341ef8 100644 --- a/src/fdosecrets/objects/Collection.cpp +++ b/src/fdosecrets/objects/Collection.cpp @@ -19,6 +19,7 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Service.h" @@ -40,7 +41,7 @@ namespace FdoSecrets } Collection::Collection(Service* parent, DatabaseWidget* backend) - : DBusObjectHelper(parent) + : DBusObject(parent) , m_backend(backend) , m_exposedGroup(nullptr) { @@ -72,23 +73,14 @@ namespace FdoSecrets m_items.first()->doDelete(); } cleanupConnections(); - unregisterPrimaryPath(); + dbus()->unregisterObject(this); // make sure we have updated copy of the filepath, which is used to identify the database. m_backendPath = m_backend->database()->canonicalFilePath(); // register the object, handling potentially duplicated name - auto name = encodePath(this->name()); - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), name); - if (!registerWithPath(path)) { - // try again with a suffix - name += QStringLiteral("_%1").arg(Tools::uuidToHex(QUuid::createUuid()).left(4)); - path = QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), name); - - if (!registerWithPath(path)) { - service()->plugin()->emitError(tr("Failed to register database on DBus under the name '%1'").arg(name)); - return false; - } + if (!dbus()->registerObject(this)) { + return false; } // populate contents after expose on dbus, because items rely on parent's dbus object path @@ -98,6 +90,7 @@ namespace FdoSecrets cleanupConnections(); } + emit collectionChanged(); return true; } @@ -108,52 +101,55 @@ namespace FdoSecrets } } - DBusReturn Collection::ensureBackend() const + DBusResult Collection::ensureBackend() const { if (!m_backend) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return {}; } - DBusReturn Collection::ensureUnlocked() const + DBusResult Collection::ensureUnlocked() const { if (backendLocked()) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); } return {}; } - DBusReturn> Collection::items() const + DBusResult Collection::items(QList& items) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return m_items; + items = m_items; + return {}; } - DBusReturn Collection::label() const + DBusResult Collection::label(QString& label) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } if (backendLocked()) { - return name(); + label = name(); + } else { + label = m_backend->database()->metadata()->name(); } - return m_backend->database()->metadata()->name(); + return {}; } - DBusReturn Collection::setLabel(const QString& label) + DBusResult Collection::setLabel(const QString& label) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -161,82 +157,87 @@ namespace FdoSecrets return {}; } - DBusReturn Collection::locked() const + DBusResult Collection::locked(bool& locked) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return backendLocked(); + locked = backendLocked(); + return {}; } - DBusReturn Collection::created() const + DBusResult Collection::created(qulonglong& created) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() - / 1000); + created = static_cast( + m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + + return {}; } - DBusReturn Collection::modified() const + DBusResult Collection::modified(qulonglong& modified) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } // FIXME: there seems not to have a global modified time. // Use a more accurate time, considering all metadata, group, entry. - return static_cast( + modified = static_cast( m_backend->database()->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Collection::deleteCollection() + DBusResult Collection::remove(const DBusClientPtr& client, PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } // Delete means close database - auto dpret = DeleteCollectionPrompt::Create(service(), this); - if (dpret.isError()) { - return dpret; + prompt = PromptBase::Create(service(), this); + if (!prompt) { + return QDBusError::InternalError; } - auto prompt = dpret.value(); if (backendLocked()) { // this won't raise a dialog, immediate execute - auto pret = prompt->prompt({}); - if (pret.isError()) { - return pret; + ret = prompt->prompt(client, {}); + if (ret.err()) { + return ret; } prompt = nullptr; } // defer the close to the prompt - return prompt; + return {}; } - DBusReturn> Collection::searchItems(const StringStringMap& attributes) + DBusResult Collection::searchItems(const StringStringMap& attributes, QList& items) { + items.clear(); + auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { // searchItems should work, whether `this` is locked or not. // however, we can't search items the same way as in gnome-keying, // because there's no database at all when locked. - return QList{}; + return {}; } // shortcut logic for Uuid/Path attributes, as they can uniquely identify an item. @@ -244,20 +245,18 @@ namespace FdoSecrets auto uuid = QUuid::fromRfc4122(QByteArray::fromHex(attributes.value(ItemAttributes::UuidKey).toLatin1())); auto entry = m_exposedGroup->findEntryByUuid(uuid); if (entry) { - return QList{m_entryToItem.value(entry)}; - } else { - return QList{}; + items += m_entryToItem.value(entry); } + return {}; } if (attributes.contains(ItemAttributes::PathKey)) { auto path = attributes.value(ItemAttributes::PathKey); auto entry = m_exposedGroup->findEntryByPath(path); if (entry) { - return QList{m_entryToItem.value(entry)}; - } else { - return QList{}; + items += m_entryToItem.value(entry); } + return {}; } QList terms; @@ -265,13 +264,12 @@ namespace FdoSecrets terms << attributeToTerm(it.key(), it.value()); } - QList items; const auto foundEntries = EntrySearcher(false, true).search(terms, m_exposedGroup); items.reserve(foundEntries.size()); for (const auto& entry : foundEntries) { items << m_entryToItem.value(entry); } - return items; + return {}; } EntrySearcher::SearchTerm Collection::attributeToTerm(const QString& key, const QString& value) @@ -296,99 +294,58 @@ namespace FdoSecrets return term; } - DBusReturn - Collection::createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt) + DBusResult Collection::createItem(const QVariantMap& properties, + const Secret& secret, + bool replace, + Item*& item, + PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - if (!pathToObject(secret.session)) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); - } - - prompt = nullptr; - - bool newlyCreated = true; - Item* item = nullptr; + item = nullptr; QString itemPath; - StringStringMap attributes; - auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")); + auto iterAttr = properties.find(DBUS_INTERFACE_SECRET_ITEM + ".Attributes"); if (iterAttr != properties.end()) { - attributes = iterAttr.value().value(); + // the actual value in iterAttr.value() is QDBusArgument, which represents a structure + // and qt has no idea what this corresponds to. + // we thus force a conversion to StringStringMap here. The conversion is registered in + // DBusTypes.cpp + auto attributes = iterAttr.value().value(); itemPath = attributes.value(ItemAttributes::PathKey); // check existing item using attributes - auto existing = searchItems(attributes); - if (existing.isError()) { - return existing; + QList existing; + ret = searchItems(attributes, existing); + if (ret.err()) { + return ret; } - if (!existing.value().isEmpty() && replace) { - item = existing.value().front(); - newlyCreated = false; + if (!existing.isEmpty() && replace) { + item = existing.front(); } } - if (!item) { - // normalize itemPath - itemPath = itemPath.startsWith('/') ? QString{} : QStringLiteral("/") + itemPath; - - // split itemPath to groupPath and itemName - auto components = itemPath.split('/'); - Q_ASSERT(components.size() >= 2); - - auto itemName = components.takeLast(); - Group* group = findCreateGroupByPath(components.join('/')); - - // create new Entry in backend - auto* entry = new Entry(); - entry->setUuid(QUuid::createUuid()); - entry->setTitle(itemName); - entry->setUsername(m_backend->database()->metadata()->defaultUserName()); - group->applyGroupIconOnCreateTo(entry); - - entry->setGroup(group); - - // when creation finishes in backend, we will already have item - item = m_entryToItem.value(entry, nullptr); - - if (!item) { - // may happen if entry somehow ends up in recycle bin - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); - } + prompt = PromptBase::Create(service(), this, properties, secret, itemPath, item); + if (!prompt) { + return QDBusError::InternalError; } - - ret = item->setProperties(properties); - if (ret.isError()) { - if (newlyCreated) { - item->doDelete(); - } - return ret; - } - ret = item->setSecret(secret); - if (ret.isError()) { - if (newlyCreated) { - item->doDelete(); - } - return ret; - } - - return item; + return {}; } - DBusReturn Collection::setProperties(const QVariantMap& properties) + DBusResult Collection::setProperties(const QVariantMap& properties) { - auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION ".Label")).toString(); + auto label = properties.value(DBUS_INTERFACE_SECRET_COLLECTION + ".Label").toString(); auto ret = setLabel(label); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -400,10 +357,10 @@ namespace FdoSecrets return m_aliases; } - DBusReturn Collection::addAlias(QString alias) + DBusResult Collection::addAlias(QString alias) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -415,22 +372,20 @@ namespace FdoSecrets emit aliasAboutToAdd(alias); - bool ok = - registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias), false); - if (ok) { + if (dbus()->registerAlias(this, alias)) { m_aliases.insert(alias); emit aliasAdded(alias); } else { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); + return QDBusError::InvalidObjectPath; } return {}; } - DBusReturn Collection::removeAlias(QString alias) + DBusResult Collection::removeAlias(QString alias) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -440,9 +395,7 @@ namespace FdoSecrets return {}; } - QDBusConnection::sessionBus().unregisterObject( - QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias)); - + dbus()->unregisterAlias(alias); m_aliases.remove(alias); emit aliasRemoved(alias); @@ -470,14 +423,11 @@ namespace FdoSecrets void Collection::onDatabaseLockChanged() { - auto locked = backendLocked(); - if (!locked) { - populateContents(); - } else { - cleanupConnections(); + if (!reloadBackend()) { + doDelete(); + return; } - emit collectionLockChanged(locked); - emit collectionChanged(); + emit collectionLockChanged(backendLocked()); } void Collection::populateContents() @@ -550,6 +500,8 @@ namespace FdoSecrets onEntryAdded(entry, false); } + // Do not connect to databaseModified signal because we only want signals for the subset under m_exposedGroup + connect(m_backend->database()->metadata(), &Metadata::metadataModified, this, &Collection::collectionChanged); connectGroupSignalRecursive(m_exposedGroup); } @@ -641,7 +593,8 @@ namespace FdoSecrets emit collectionAboutToDelete(); - unregisterPrimaryPath(); + // remove from dbus early + dbus()->unregisterObject(this); // remove alias manually to trigger signal for (const auto& a : aliases()) { @@ -692,7 +645,7 @@ namespace FdoSecrets void Collection::doDeleteEntries(QList entries) { - m_backend->deleteEntries(std::move(entries)); + m_backend->deleteEntries(std::move(entries), FdoSecrets::settings()->confirmDeleteItem()); } Group* Collection::findCreateGroupByPath(const QString& groupPath) @@ -748,4 +701,36 @@ namespace FdoSecrets return inRecycleBin(entry->group()); } + Item* Collection::doNewItem(const DBusClientPtr& client, QString itemPath) + { + Q_ASSERT(m_backend); + + // normalize itemPath + itemPath = (itemPath.startsWith('/') ? QString{} : QStringLiteral("/")) + itemPath; + + // split itemPath to groupPath and itemName + auto components = itemPath.split('/'); + Q_ASSERT(components.size() >= 2); + + auto itemName = components.takeLast(); + Group* group = findCreateGroupByPath(components.join('/')); + + // create new Entry in backend + auto* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle(itemName); + entry->setUsername(m_backend->database()->metadata()->defaultUserName()); + group->applyGroupIconOnCreateTo(entry); + + entry->setGroup(group); + + // the item was just created so there is no point in having it not authorized + client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed); + + // when creation finishes in backend, we will already have item + auto created = m_entryToItem.value(entry, nullptr); + + return created; + } + } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h index 80940d5a..d80fc0e3 100644 --- a/src/fdosecrets/objects/Collection.h +++ b/src/fdosecrets/objects/Collection.h @@ -18,9 +18,9 @@ #ifndef KEEPASSXC_FDOSECRETS_COLLECTION_H #define KEEPASSXC_FDOSECRETS_COLLECTION_H -#include "DBusObject.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" -#include "adaptors/CollectionAdaptor.h" #include "core/EntrySearcher.h" #include @@ -36,9 +36,10 @@ namespace FdoSecrets class Item; class PromptBase; class Service; - class Collection : public DBusObjectHelper + class Collection : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_COLLECTION_LITERAL) explicit Collection(Service* parent, DatabaseWidget* backend); @@ -54,21 +55,21 @@ namespace FdoSecrets */ static Collection* Create(Service* parent, DatabaseWidget* backend); - DBusReturn> items() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult items(QList& items) const; - DBusReturn label() const; - DBusReturn setLabel(const QString& label); + Q_INVOKABLE DBUS_PROPERTY DBusResult label(QString& label) const; + Q_INVOKABLE DBusResult setLabel(const QString& label); - DBusReturn locked() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult locked(bool& locked) const; - DBusReturn created() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult created(qulonglong& created) const; - DBusReturn modified() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult modified(qulonglong& modified) const; - DBusReturn deleteCollection(); - DBusReturn> searchItems(const StringStringMap& attributes); - DBusReturn - createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt); + Q_INVOKABLE DBusResult remove(const DBusClientPtr& client, PromptBase*& prompt); + Q_INVOKABLE DBusResult searchItems(const StringStringMap& attributes, QList& items); + Q_INVOKABLE DBusResult + createItem(const QVariantMap& properties, const Secret& secret, bool replace, Item*& item, PromptBase*& prompt); signals: void itemCreated(Item* item); @@ -86,15 +87,15 @@ namespace FdoSecrets void doneUnlockCollection(bool accepted); public: - DBusReturn setProperties(const QVariantMap& properties); + DBusResult setProperties(const QVariantMap& properties); bool isValid() const { return backend(); } - DBusReturn removeAlias(QString alias); - DBusReturn addAlias(QString alias); + DBusResult removeAlias(QString alias); + DBusResult addAlias(QString alias); const QSet aliases() const; /** @@ -116,6 +117,7 @@ namespace FdoSecrets // expose some methods for Prompt to use bool doLock(); void doUnlock(); + Item* doNewItem(const DBusClientPtr& client, QString itemPath); // will remove self void doDelete(); @@ -147,13 +149,13 @@ namespace FdoSecrets * Check if the backend is a valid object, send error reply if not. * @return true if the backend is valid. */ - DBusReturn ensureBackend() const; + DBusResult ensureBackend() const; /** * Ensure the database is unlocked, send error reply if locked. * @return true if the database is locked */ - DBusReturn ensureUnlocked() const; + DBusResult ensureUnlocked() const; /** * Like mkdir -p, find or create the group by path, under m_exposedGroup diff --git a/src/fdosecrets/objects/DBusObject.h b/src/fdosecrets/objects/DBusObject.h deleted file mode 100644 index d51642a8..00000000 --- a/src/fdosecrets/objects/DBusObject.h +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_DBUSOBJECT_H -#define KEEPASSXC_FDOSECRETS_DBUSOBJECT_H - -#include "fdosecrets/objects/DBusReturn.h" -#include "fdosecrets/objects/DBusTypes.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace FdoSecrets -{ - class Service; - - /** - * @brief A common base class for all dbus-exposed objects. - * However, derived class should inherit from `DBusObjectHelper`, which is - * the only way to set DBus adaptor and enforces correct adaptor creation. - */ - class DBusObject : public QObject, public QDBusContext - { - Q_OBJECT - public: - const QDBusObjectPath& objectPath() const - { - return m_objectPath; - } - - QDBusAbstractAdaptor& dbusAdaptor() const - { - return *m_dbusAdaptor; - } - - protected: - /** - * @brief Register this object at given DBus path - * @param path DBus path to register at - * @param primary whether this path to be considered primary. The primary path is the one to be returned by - * `DBusObject::objectPath`. - * @return true on success - */ - bool registerWithPath(const QString& path, bool primary = true); - - void unregisterPrimaryPath() - { - if (m_objectPath.path() == QStringLiteral("/")) { - return; - } - QDBusConnection::sessionBus().unregisterObject(m_objectPath.path()); - m_objectPath.setPath(QStringLiteral("/")); - } - - QString callingPeer() const - { - Q_ASSERT(calledFromDBus()); - return message().service(); - } - - uint callingPeerPid() const - { - return connection().interface()->servicePid(callingPeer()); - } - - QString callingPeerName() const; - - DBusObject* p() const - { - return qobject_cast(parent()); - } - - private: - explicit DBusObject(DBusObject* parent); - - /** - * Derived class should not directly use sendErrorReply. - * Instead, use raiseError - */ - using QDBusContext::sendErrorReply; - - template friend class DBusReturn; - template friend class DBusObjectHelper; - - QDBusAbstractAdaptor* m_dbusAdaptor; - QDBusObjectPath m_objectPath; - }; - - template class DBusObjectHelper : public DBusObject - { - protected: - explicit DBusObjectHelper(DBusObject* parent) - : DBusObject(parent) - { - // creating new Adaptor has to be delayed into constructor's body, - // and can't be simply moved to initializer list, because at that - // point the base QObject class hasn't been initialized and will sigfault. - m_dbusAdaptor = new Adaptor(static_cast(this)); - m_dbusAdaptor->setParent(this); - } - }; - - /** - * Return the object path of the pointed DBusObject, or "/" if the pointer is null - * @tparam T - * @param object - * @return - */ - template QDBusObjectPath objectPathSafe(T* object) - { - if (object) { - return object->objectPath(); - } - return QDBusObjectPath(QStringLiteral("/")); - } - - /** - * Convert a list of DBusObjects to object path - * @tparam T - * @param objects - * @return - */ - template QList objectsToPath(QList objects) - { - QList res; - res.reserve(objects.size()); - for (auto object : objects) { - res.append(objectPathSafe(object)); - } - return res; - } - - /** - * Convert an object path to a pointer of the object - * @tparam T - * @param path - * @return the pointer of the object, or nullptr if path is "/" - */ - template T* pathToObject(const QDBusObjectPath& path) - { - if (path.path() == QStringLiteral("/")) { - return nullptr; - } - return qobject_cast(QDBusConnection::sessionBus().objectRegisteredAt(path.path())); - } - - /** - * Convert a list of object paths to a list of objects. - * "/" paths (i.e. nullptrs) will be skipped in the resulting list - * @tparam T - * @param paths - * @return - */ - template QList pathsToObject(const QList& paths) - { - QList res; - res.reserve(paths.size()); - for (const auto& path : paths) { - auto object = pathToObject(path); - if (object) { - res.append(object); - } - } - return res; - } - - /** - * Encode the string value to a DBus object path safe representation, - * using a schema similar to URI encoding, but with percentage(%) replaced with - * underscore(_). All characters except [A-Za-z0-9] are encoded. For non-ascii - * characters, UTF-8 encoding is first applied and each of the resulting byte - * value is encoded. - * @param value - * @return encoded string - */ - QString encodePath(const QString& value); - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H diff --git a/src/fdosecrets/objects/DBusReturn.cpp b/src/fdosecrets/objects/DBusReturn.cpp deleted file mode 100644 index ffd10add..00000000 --- a/src/fdosecrets/objects/DBusReturn.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "DBusReturn.h" diff --git a/src/fdosecrets/objects/DBusReturn.h b/src/fdosecrets/objects/DBusReturn.h deleted file mode 100644 index 889b8e11..00000000 --- a/src/fdosecrets/objects/DBusReturn.h +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_DBUSRETURN_H -#define KEEPASSXC_FDOSECRETS_DBUSRETURN_H - -#include -#include -#include - -#include - -namespace FdoSecrets -{ - - namespace details - { - class DBusReturnImpl - { - public: - /** - * Check if this object contains an error - * @return true if it contains an error, false otherwise. - */ - bool isError() const - { - return !m_errorName.isEmpty(); - } - - /** - * Get the error name - * @return - */ - QString errorName() const - { - return m_errorName; - } - - void okOrDie() const - { - Q_ASSERT(!isError()); - } - - protected: - struct WithErrorTag - { - }; - - /** - * Construct from an error - * @param errorName - * @param value - */ - DBusReturnImpl(QString errorName, WithErrorTag) - : m_errorName(std::move(errorName)) - { - } - - DBusReturnImpl() = default; - - protected: - QString m_errorName; - }; - } // namespace details - - /** - * Either a return value or a DBus error - * @tparam T - */ - template class DBusReturn : public details::DBusReturnImpl - { - protected: - using DBusReturnImpl::DBusReturnImpl; - - public: - using value_type = T; - - DBusReturn() = default; - - /** - * Implicitly construct from a value - * @param value - */ - DBusReturn(T&& value) // NOLINT(google-explicit-constructor) - : m_value(std::move(value)) - { - } - - DBusReturn(const T& value) // NOLINT(google-explicit-constructor) - : m_value(std::move(value)) - { - } - - /** - * Implicitly convert from another error of different value type. - * - * @tparam U must not be the same as T - * @param other - */ - template ::value>::type> - DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor) - : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{}) - { - Q_ASSERT(other.isError()); - } - - /** - * Construct from error - * @param errorType - * @return a DBusReturn object containing the error - */ - static DBusReturn Error(QDBusError::ErrorType errorType) - { - return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}}; - } - - /** - * Overloaded version - * @param errorName - * @return a DBusReturnImpl object containing the error - */ - static DBusReturn Error(QString errorName) - { - return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}}; - } - - /** - * Get a reference to the enclosed value - * @return - */ - const T& value() const& - { - okOrDie(); - return m_value; - } - - /** - * Get a rvalue reference to the enclosed value if this object is rvalue - * @return a rvalue reference to the enclosed value - */ - T value() && - { - okOrDie(); - return std::move(m_value); - } - - /** - * Get value or handle the error by the passed in dbus object - * @tparam P - * @param p - * @return - */ - template T valueOrHandle(P* p) const& - { - if (isError()) { - if (p->calledFromDBus()) { - p->sendErrorReply(errorName()); - } - return {}; - } - return m_value; - } - - /** - * Get value or handle the error by the passed in dbus object - * @tparam P - * @param p - * @return - */ - template T&& valueOrHandle(P* p) && - { - if (isError()) { - if (p->calledFromDBus()) { - p->sendErrorReply(errorName()); - } - } - return std::move(m_value); - } - - private: - T m_value{}; - }; - - template <> class DBusReturn : public details::DBusReturnImpl - { - protected: - using DBusReturnImpl::DBusReturnImpl; - - public: - using value_type = void; - - DBusReturn() = default; - - /** - * Implicitly convert from another error of different value type. - * - * @tparam U must not be the same as T - * @param other - */ - template ::value>::type> - DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor) - : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{}) - { - Q_ASSERT(other.isError()); - } - - /** - * Construct from error - * @param errorType - * @return a DBusReturn object containing the error - */ - static DBusReturn Error(QDBusError::ErrorType errorType) - { - return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}}; - } - - /** - * Overloaded version - * @param errorName - * @return a DBusReturnImpl object containing the error - */ - static DBusReturn Error(QString errorName) - { - return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}}; - } - - /** - * If this is return contains an error, handle it if we were called from DBus - * @tparam P - * @param p - */ - template void handle(P* p) const - { - if (isError()) { - if (p->calledFromDBus()) { - p->sendErrorReply(errorName()); - } - } - } - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_DBUSRETURN_H diff --git a/src/fdosecrets/objects/DBusTypes.cpp b/src/fdosecrets/objects/DBusTypes.cpp deleted file mode 100644 index c249eaee..00000000 --- a/src/fdosecrets/objects/DBusTypes.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * Copyright 2010, Michael Leupold - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "DBusTypes.h" - -#include - -namespace FdoSecrets -{ - - void registerDBusTypes() - { - // register meta-types needed for this adaptor - qRegisterMetaType(); - qDBusRegisterMetaType(); - - qRegisterMetaType(); - qDBusRegisterMetaType(); - - qRegisterMetaType(); - qDBusRegisterMetaType(); - - QMetaType::registerConverter([](const QDBusArgument& arg) { - if (arg.currentSignature() != "a{ss}") { - return StringStringMap{}; - } - // QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const. - // we don't want to modify the instance (arg) stored in the qvariant so we create a copy - const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization) - return qdbus_cast(copy); - }); - - // NOTE: this is already registered by Qt in qtextratypes.h - // qRegisterMetaType >(); - // qDBusRegisterMetaType >(); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/DBusTypes.h b/src/fdosecrets/objects/DBusTypes.h deleted file mode 100644 index ef1e2276..00000000 --- a/src/fdosecrets/objects/DBusTypes.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * Copyright 2010, Michael Leupold - * Copyright 2010-2011, Valentin Rusu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_DBUSTYPES_H -#define KEEPASSXC_FDOSECRETS_DBUSTYPES_H - -#include -#include -#include -#include - -#define DBUS_SERVICE_SECRET "org.freedesktop.secrets" - -#define DBUS_INTERFACE_SECRET_SERVICE "org.freedesktop.Secret.Service" -#define DBUS_INTERFACE_SECRET_SESSION "org.freedesktop.Secret.Session" -#define DBUS_INTERFACE_SECRET_COLLECTION "org.freedesktop.Secret.Collection" -#define DBUS_INTERFACE_SECRET_ITEM "org.freedesktop.Secret.Item" -#define DBUS_INTERFACE_SECRET_PROMPT "org.freedesktop.Secret.Prompt" - -#define DBUS_ERROR_SECRET_NO_SESSION "org.freedesktop.Secret.Error.NoSession" -#define DBUS_ERROR_SECRET_NO_SUCH_OBJECT "org.freedesktop.Secret.Error.NoSuchObject" -#define DBUS_ERROR_SECRET_IS_LOCKED "org.freedesktop.Secret.Error.IsLocked" - -#define DBUS_PATH_SECRETS "/org/freedesktop/secrets" - -#define DBUS_PATH_TEMPLATE_ALIAS "%1/aliases/%2" -#define DBUS_PATH_TEMPLATE_SESSION "%1/session/%2" -#define DBUS_PATH_TEMPLATE_COLLECTION "%1/collection/%2" -#define DBUS_PATH_TEMPLATE_ITEM "%1/%2" -#define DBUS_PATH_TEMPLATE_PROMPT "%1/prompt/%2" - -namespace FdoSecrets -{ - /** - * This is the basic Secret structure exchanged via the dbus API - * See the spec for more details - */ - struct SecretStruct - { - QDBusObjectPath session{}; - QByteArray parameters{}; - QByteArray value{}; - QString contentType{}; - }; - - inline QDBusArgument& operator<<(QDBusArgument& argument, const SecretStruct& secret) - { - argument.beginStructure(); - argument << secret.session << secret.parameters << secret.value << secret.contentType; - argument.endStructure(); - return argument; - } - - inline const QDBusArgument& operator>>(const QDBusArgument& argument, SecretStruct& secret) - { - argument.beginStructure(); - argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType; - argument.endStructure(); - return argument; - } - - /** - * Register the types needed for the fd.o Secrets D-Bus interface. - */ - void registerDBusTypes(); - -} // namespace FdoSecrets - -typedef QMap StringStringMap; -typedef QMap ObjectPathSecretMap; - -Q_DECLARE_METATYPE(FdoSecrets::SecretStruct) -Q_DECLARE_METATYPE(StringStringMap); -Q_DECLARE_METATYPE(ObjectPathSecretMap); - -#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp index adf4f3d4..b7964937 100644 --- a/src/fdosecrets/objects/Item.cpp +++ b/src/fdosecrets/objects/Item.cpp @@ -18,6 +18,7 @@ #include "Item.h" #include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Service.h" @@ -40,7 +41,7 @@ namespace FdoSecrets const QSet Item::ReadOnlyAttributes(QSet() << ItemAttributes::UuidKey << ItemAttributes::PathKey); static void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType); - static SecretStruct getEntrySecret(Entry* entry); + static Secret getEntrySecret(Entry* entry); namespace { @@ -51,8 +52,7 @@ namespace FdoSecrets Item* Item::Create(Collection* parent, Entry* backend) { QScopedPointer res{new Item(parent, backend)}; - - if (!res->registerSelf()) { + if (!res->dbus()->registerObject(res.data())) { return nullptr; } @@ -60,46 +60,37 @@ namespace FdoSecrets } Item::Item(Collection* parent, Entry* backend) - : DBusObjectHelper(parent) + : DBusObject(parent) , m_backend(backend) { - Q_ASSERT(!p()->objectPath().path().isEmpty()); - connect(m_backend.data(), &Entry::entryModified, this, &Item::itemChanged); } - bool Item::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_ITEM).arg(p()->objectPath().path(), m_backend->uuidToHex()); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register item on DBus at path '%1'").arg(path)); - } - return ok; - } - - DBusReturn Item::locked() const + DBusResult Item::locked(const DBusClientPtr& client, bool& locked) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return collection()->locked(); + ret = collection()->locked(locked); + if (ret.err()) { + return ret; + } + locked = locked || !client->itemAuthorized(m_backend->uuid()); + return {}; } - DBusReturn Item::attributes() const + DBusResult Item::attributes(StringStringMap& attrs) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - StringStringMap attrs; - // add default attributes except password auto entryAttrs = m_backend->attributes(); for (const auto& attr : EntryAttributes::DefaultAttributes) { @@ -124,17 +115,17 @@ namespace FdoSecrets // add some informative and readonly attributes attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex(); attrs[ItemAttributes::PathKey] = path(); - return attrs; + return {}; } - DBusReturn Item::setAttributes(const StringStringMap& attrs) + DBusResult Item::setAttributes(const StringStringMap& attrs) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -158,28 +149,29 @@ namespace FdoSecrets return {}; } - DBusReturn Item::label() const + DBusResult Item::label(QString& label) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return m_backend->title(); + label = m_backend->title(); + return {}; } - DBusReturn Item::setLabel(const QString& label) + DBusResult Item::setLabel(const QString& label) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -190,91 +182,106 @@ namespace FdoSecrets return {}; } - DBusReturn Item::created() const + DBusResult Item::created(qulonglong& created) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + created = static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Item::modified() const + DBusResult Item::modified(qulonglong& modified) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + modified = static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Item::deleteItem() + DBusResult Item::remove(PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - auto prompt = DeleteItemPrompt::Create(service(), this); - return prompt.value(); + prompt = PromptBase::Create(service(), this); + if (!prompt) { + return QDBusError::InternalError; + } + return {}; } - DBusReturn Item::getSecret(Session* session) + DBusResult Item::getSecret(const DBusClientPtr& client, Session* session, Secret& secret) + { + auto ret = getSecretNoNotification(client, session, secret); + if (ret.ok()) { + service()->plugin()->emitRequestShowNotification( + tr(R"(Entry "%1" from database "%2" was used by %3)") + .arg(m_backend->title(), collection()->name(), client->name())); + } + return ret; + } + + DBusResult Item::getSecretNoNotification(const DBusClientPtr& client, Session* session, Secret& secret) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } + if (!client->itemAuthorizedResetOnce(backend()->uuid())) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); + } if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } - auto secret = getEntrySecret(m_backend); + secret = getEntrySecret(m_backend); // encode using session secret = session->encode(secret); - // show notification is this was directly called from DBus - if (calledFromDBus()) { - service()->plugin()->emitRequestShowNotification( - tr(R"(Entry "%1" from database "%2" was used by %3)") - .arg(m_backend->title(), collection()->name(), callingPeerName())); - } - return secret; + return {}; } - DBusReturn Item::setSecret(const SecretStruct& secret) + DBusResult Item::setSecret(const DBusClientPtr& client, const Secret& secret) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } + if (!client->itemAuthorizedResetOnce(backend()->uuid())) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); + } - auto session = pathToObject(secret.session); - if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + if (!secret.session) { + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } // decode using session - auto decoded = session->decode(secret); + auto decoded = secret.session->decode(secret); // set in backend m_backend->beginUpdate(); @@ -284,19 +291,18 @@ namespace FdoSecrets return {}; } - DBusReturn Item::setProperties(const QVariantMap& properties) + DBusResult Item::setProperties(const QVariantMap& properties) { - auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Label")).toString(); + auto label = properties.value(DBUS_INTERFACE_SECRET_ITEM + ".Label").toString(); auto ret = setLabel(label); - if (ret.isError()) { + if (ret.err()) { return ret; } - auto attributes = - properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value(); + auto attributes = properties.value(DBUS_INTERFACE_SECRET_ITEM + ".Attributes").value(); ret = setAttributes(attributes); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -305,25 +311,26 @@ namespace FdoSecrets Collection* Item::collection() const { - return qobject_cast(p()); + return qobject_cast(parent()); } - DBusReturn Item::ensureBackend() const + DBusResult Item::ensureBackend() const { if (!m_backend) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return {}; } - DBusReturn Item::ensureUnlocked() const + DBusResult Item::ensureUnlocked() const { - auto locked = collection()->locked(); - if (locked.isError()) { - return locked; + bool l; + auto ret = collection()->locked(l); + if (ret.err()) { + return ret; } - if (locked.value()) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + if (l) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); } return {}; } @@ -340,7 +347,7 @@ namespace FdoSecrets // Unregister current path early, do not rely on deleteLater's call to destructor // as in case of Entry moving between groups, new Item will be created at the same DBus path // before the current Item is deleted in the event loop. - unregisterPrimaryPath(); + dbus()->unregisterObject(this); m_backend = nullptr; deleteLater(); @@ -369,13 +376,6 @@ namespace FdoSecrets return pathComponents.join('/'); } - bool Item::isDeletePermanent() const - { - auto recycleBin = backend()->database()->metadata()->recycleBin(); - return (recycleBin && recycleBin->findEntryByUuid(backend()->uuid())) - || !backend()->database()->metadata()->recycleBinEnabled(); - } - void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType) { auto mimeName = contentType.split(';').takeFirst().trimmed(); @@ -414,9 +414,9 @@ namespace FdoSecrets entry->setPassword(codec->toUnicode(data)); } - SecretStruct getEntrySecret(Entry* entry) + Secret getEntrySecret(Entry* entry) { - SecretStruct ss; + Secret ss{}; if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) { ss.value = entry->attachments()->value(FDO_SECRETS_DATA); diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h index 8c753a3d..f246a31e 100644 --- a/src/fdosecrets/objects/Item.h +++ b/src/fdosecrets/objects/Item.h @@ -18,8 +18,8 @@ #ifndef KEEPASSXC_FDOSECRETS_ITEM_H #define KEEPASSXC_FDOSECRETS_ITEM_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/ItemAdaptor.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" #include @@ -38,9 +38,10 @@ namespace FdoSecrets class Collection; class PromptBase; - class Item : public DBusObjectHelper + class Item : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_ITEM_LITERAL) explicit Item(Collection* parent, Entry* backend); @@ -55,21 +56,21 @@ namespace FdoSecrets */ static Item* Create(Collection* parent, Entry* backend); - DBusReturn locked() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult locked(const DBusClientPtr& client, bool& locked) const; - DBusReturn attributes() const; - DBusReturn setAttributes(const StringStringMap& attrs); + Q_INVOKABLE DBUS_PROPERTY DBusResult attributes(StringStringMap& attrs) const; + Q_INVOKABLE DBusResult setAttributes(const StringStringMap& attrs); - DBusReturn label() const; - DBusReturn setLabel(const QString& label); + Q_INVOKABLE DBUS_PROPERTY DBusResult label(QString& label) const; + Q_INVOKABLE DBusResult setLabel(const QString& label); - DBusReturn created() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult created(qulonglong& created) const; - DBusReturn modified() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult modified(qulonglong& modified) const; - DBusReturn deleteItem(); - DBusReturn getSecret(Session* session); - DBusReturn setSecret(const SecretStruct& secret); + Q_INVOKABLE DBusResult remove(PromptBase*& prompt); + Q_INVOKABLE DBusResult getSecret(const DBusClientPtr& client, Session* session, Secret& secret); + Q_INVOKABLE DBusResult setSecret(const DBusClientPtr& client, const Secret& secret); signals: void itemChanged(); @@ -78,7 +79,8 @@ namespace FdoSecrets public: static const QSet ReadOnlyAttributes; - DBusReturn setProperties(const QVariantMap& properties); + DBusResult getSecretNoNotification(const DBusClientPtr& client, Session* session, Secret& secret) const; + DBusResult setProperties(const QVariantMap& properties); Entry* backend() const; Collection* collection() const; @@ -90,39 +92,26 @@ namespace FdoSecrets */ QString path() const; - /** - * If the containing db does not have recycle bin enabled, - * or the entry is already in the recycle bin (not possible for item, though), - * the delete is permanent - * @return true if delete is permanent - */ - bool isDeletePermanent() const; - public slots: void doDelete(); - /** - * @brief Register self on DBus - * @return - */ - bool registerSelf(); - /** * Check if the backend is a valid object, send error reply if not. * @return No error if the backend is valid. */ - DBusReturn ensureBackend() const; + DBusResult ensureBackend() const; /** * Ensure the database is unlocked, send error reply if locked. * @return true if the database is locked */ - DBusReturn ensureUnlocked() const; + DBusResult ensureUnlocked() const; private: QPointer m_backend; }; } // namespace FdoSecrets +Q_DECLARE_METATYPE(FdoSecrets::ItemSecretMap); #endif // KEEPASSXC_FDOSECRETS_ITEM_H diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp index efed6317..78cc3efa 100644 --- a/src/fdosecrets/objects/Prompt.cpp +++ b/src/fdosecrets/objects/Prompt.cpp @@ -18,10 +18,12 @@ #include "Prompt.h" #include "fdosecrets/FdoSecretsPlugin.h" -#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" +#include "fdosecrets/widgets/AccessControlDialog.h" #include "core/Tools.h" #include "gui/DatabaseWidget.h" @@ -29,27 +31,17 @@ #include #include +#include namespace FdoSecrets { PromptBase::PromptBase(Service* parent) - : DBusObjectHelper(parent) + : DBusObject(parent) { connect(this, &PromptBase::completed, this, &PromptBase::deleteLater); } - bool PromptBase::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_PROMPT) - .arg(p()->objectPath().path(), Tools::uuidToHex(QUuid::createUuid())); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register item on DBus at path '%1'").arg(path)); - } - return ok; - } - QWindow* PromptBase::findWindow(const QString& windowId) { // find parent window, or nullptr if not found @@ -71,41 +63,29 @@ namespace FdoSecrets return qobject_cast(parent()); } - DBusReturn PromptBase::dismiss() + DBusResult PromptBase::dismiss() { emit completed(true, ""); return {}; } - DBusReturn DeleteCollectionPrompt::Create(Service* parent, Collection* coll) - { - QScopedPointer res{new DeleteCollectionPrompt(parent, coll)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll) : PromptBase(parent) , m_collection(coll) { } - DBusReturn DeleteCollectionPrompt::prompt(const QString& windowId) + DBusResult DeleteCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } if (!m_collection) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } MessageBox::OverrideParent override(findWindow(windowId)); @@ -117,29 +97,19 @@ namespace FdoSecrets return {}; } - DBusReturn CreateCollectionPrompt::Create(Service* parent) - { - QScopedPointer res{new CreateCollectionPrompt(parent)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - - CreateCollectionPrompt::CreateCollectionPrompt(Service* parent) + CreateCollectionPrompt::CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias) : PromptBase(parent) + , m_properties(std::move(properties)) + , m_alias(std::move(alias)) { } - DBusReturn CreateCollectionPrompt::prompt(const QString& windowId) + DBusResult CreateCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -150,27 +120,30 @@ namespace FdoSecrets return dismiss(); } - emit collectionCreated(coll); + auto ret = coll->setProperties(m_properties); + if (ret.err()) { + coll->doDelete(); + return dismiss(); + } + if (!m_alias.isEmpty()) { + ret = coll->addAlias(m_alias); + if (ret.err()) { + coll->doDelete(); + return dismiss(); + } + } + emit completed(false, QVariant::fromValue(coll->objectPath())); return {}; } - DBusReturn CreateCollectionPrompt::dismiss() + DBusResult CreateCollectionPrompt::dismiss() { - emit completed(true, QVariant::fromValue(QDBusObjectPath{"/"})); + emit completed(true, QVariant::fromValue(DBusMgr::objectPathSafe(nullptr))); return {}; } - DBusReturn LockCollectionsPrompt::Create(Service* parent, const QList& colls) - { - QScopedPointer res{new LockCollectionsPrompt(parent, colls)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList& colls) : PromptBase(parent) { @@ -180,15 +153,12 @@ namespace FdoSecrets } } - DBusReturn LockCollectionsPrompt::prompt(const QString& windowId) + DBusResult LockCollectionsPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -208,113 +178,177 @@ namespace FdoSecrets return {}; } - DBusReturn LockCollectionsPrompt::dismiss() + DBusResult LockCollectionsPrompt::dismiss() { emit completed(true, QVariant::fromValue(m_locked)); return {}; } - DBusReturn UnlockCollectionsPrompt::Create(Service* parent, - const QList& coll) - { - QScopedPointer res{new UnlockCollectionsPrompt(parent, coll)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - - UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList& colls) + UnlockPrompt::UnlockPrompt(Service* parent, const QSet& colls, const QSet& items) : PromptBase(parent) { m_collections.reserve(colls.size()); - for (const auto& c : asConst(colls)) { - m_collections << c; + for (const auto& coll : asConst(colls)) { + m_collections << coll; + connect(coll, &Collection::doneUnlockCollection, this, &UnlockPrompt::collectionUnlockFinished); + } + for (const auto& item : asConst(items)) { + m_items[item->collection()] << item; } } - DBusReturn UnlockCollectionsPrompt::prompt(const QString& windowId) + DBusResult UnlockPrompt::prompt(const DBusClientPtr& client, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } MessageBox::OverrideParent override(findWindow(windowId)); + // for use in unlockItems + m_windowId = windowId; + m_client = client; + + // first unlock any collections + bool waitingForCollections = false; for (const auto& c : asConst(m_collections)) { if (c) { - // doUnlock is nonblocking - connect(c, &Collection::doneUnlockCollection, this, &UnlockCollectionsPrompt::collectionUnlockFinished); + // doUnlock is nonblocking, execution will continue in collectionUnlockFinished c->doUnlock(); + waitingForCollections = true; } } + // unlock items directly if no collection unlocking pending + // o.w. do it in collectionUnlockFinished + if (!waitingForCollections) { + // do not block the current method + QTimer::singleShot(0, this, &UnlockPrompt::unlockItems); + } + return {}; } - void UnlockCollectionsPrompt::collectionUnlockFinished(bool accepted) + void UnlockPrompt::collectionUnlockFinished(bool accepted) { auto coll = qobject_cast(sender()); if (!coll) { return; } - if (!m_collections.contains(coll)) { - // should not happen - coll->disconnect(this); - return; - } - // one shot coll->disconnect(this); + if (!m_collections.contains(coll)) { + // should not happen + return; + } + if (accepted) { m_unlocked << coll->objectPath(); } else { m_numRejected += 1; + // no longer need to unlock the item if its containing collection + // didn't unlock. + m_items.remove(coll); } - // if we've get all + // if we got response for all collections if (m_numRejected + m_unlocked.size() == m_collections.size()) { - emit completed(m_unlocked.isEmpty(), QVariant::fromValue(m_unlocked)); + // next step is to unlock items + unlockItems(); } } - DBusReturn UnlockCollectionsPrompt::dismiss() + + void UnlockPrompt::unlockItems() + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return; + } + + // flatten to list of entries + QList entries; + for (const auto& itemsPerColl : m_items.values()) { + for (const auto& item : itemsPerColl) { + if (!item) { + m_numRejected += 1; + continue; + } + auto entry = item->backend(); + if (client->itemKnown(entry->uuid())) { + if (!client->itemAuthorized(entry->uuid())) { + m_numRejected += 1; + } + continue; + } + // attach a temporary property so later we can get the item + // back from the dialog's result + entry->setProperty(FdoSecretsBackend, QVariant::fromValue(item.data())); + entries << entry; + } + } + if (!entries.isEmpty()) { + QString app = tr("%1 (PID: %2)").arg(client->name()).arg(client->pid()); + auto ac = new AccessControlDialog(findWindow(m_windowId), entries, app, AuthOption::Remember); + connect(ac, &AccessControlDialog::finished, this, &UnlockPrompt::itemUnlockFinished); + connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater); + ac->open(); + } else { + itemUnlockFinished({}); + } + } + + void UnlockPrompt::itemUnlockFinished(const QHash& decisions) + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return; + } + for (auto it = decisions.constBegin(); it != decisions.constEnd(); ++it) { + auto entry = it.key(); + // get back the corresponding item + auto item = entry->property(FdoSecretsBackend).value(); + entry->setProperty(FdoSecretsBackend, {}); + Q_ASSERT(item); + + // set auth + client->setItemAuthorized(entry->uuid(), it.value()); + + if (client->itemAuthorized(entry->uuid())) { + m_unlocked += item->objectPath(); + } else { + m_numRejected += 1; + } + } + // if anything is not unlocked, treat the whole prompt as dismissed + // so the client has a chance to handle the error + emit completed(m_numRejected > 0, QVariant::fromValue(m_unlocked)); + } + + DBusResult UnlockPrompt::dismiss() { emit completed(true, QVariant::fromValue(m_unlocked)); return {}; } - DBusReturn DeleteItemPrompt::Create(Service* parent, Item* item) - { - QScopedPointer res{new DeleteItemPrompt(parent, item)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item) : PromptBase(parent) , m_item(item) { } - DBusReturn DeleteItemPrompt::prompt(const QString& windowId) + DBusResult DeleteItemPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -322,14 +356,6 @@ namespace FdoSecrets // delete item's backend. Item will be notified after the backend is deleted. if (m_item) { - if (FdoSecrets::settings()->noConfirmDeleteItem()) { - // based on permanent or not, different button is used - if (m_item->isDeletePermanent()) { - MessageBox::setNextAnswer(MessageBox::Delete); - } else { - MessageBox::setNextAnswer(MessageBox::Move); - } - } m_item->collection()->doDeleteEntries({m_item->backend()}); } @@ -337,4 +363,121 @@ namespace FdoSecrets return {}; } + + CreateItemPrompt::CreateItemPrompt(Service* parent, + Collection* coll, + QVariantMap properties, + Secret secret, + QString itemPath, + Item* existing) + : PromptBase(parent) + , m_coll(coll) + , m_properties(std::move(properties)) + , m_secret(std::move(secret)) + , m_itemPath(std::move(itemPath)) + , m_item(existing) + // session aliveness also need to be tracked, for potential use later in updateItem + , m_sess(m_secret.session) + { + } + + DBusResult CreateItemPrompt::prompt(const DBusClientPtr& client, const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + if (!m_coll) { + return dismiss(); + } + + // save a weak reference to the client which may be used asynchronously later + m_client = client; + + // the item doesn't exists yet, create it + if (!m_item) { + m_item = m_coll->doNewItem(client, m_itemPath); + if (!m_item) { + // may happen if entry somehow ends up in recycle bin + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); + } + + auto ret = updateItem(); + if (ret.err()) { + m_item->doDelete(); + return ret; + } + emit completed(false, QVariant::fromValue(m_item->objectPath())); + } else { + bool locked = false; + auto ret = m_item->locked(client, locked); + if (ret.err()) { + return ret; + } + if (locked) { + // give the user a chance to unlock the item + auto prompt = PromptBase::Create(service(), QSet{}, QSet{m_item}); + if (!prompt) { + return QDBusError::InternalError; + } + // postpone anything after the confirmation + connect(prompt, &PromptBase::completed, this, &CreateItemPrompt::itemUnlocked); + return prompt->prompt(client, windowId); + } else { + ret = updateItem(); + if (ret.err()) { + return ret; + } + emit completed(false, QVariant::fromValue(m_item->objectPath())); + } + } + return {}; + } + + DBusResult CreateItemPrompt::dismiss() + { + emit completed(true, QVariant::fromValue(DBusMgr::objectPathSafe(nullptr))); + return {}; + } + + void CreateItemPrompt::itemUnlocked(bool dismissed, const QVariant& result) + { + auto unlocked = result.value>(); + if (!unlocked.isEmpty()) { + // in theory we should check if the object path matches m_item, but a mismatch should not happen, + // because we control the unlock prompt ourselves + updateItem(); + } + emit completed(dismissed, QVariant::fromValue(DBusMgr::objectPathSafe(m_item))); + } + + DBusResult CreateItemPrompt::updateItem() + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return {}; + } + + if (!m_sess || m_sess != m_secret.session) { + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); + } + if (!m_item) { + return {}; + } + auto ret = m_item->setProperties(m_properties); + if (ret.err()) { + return ret; + } + ret = m_item->setSecret(client, m_secret); + if (ret.err()) { + return ret; + } + return {}; + } } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h index 9a972567..ea5afc5d 100644 --- a/src/fdosecrets/objects/Prompt.h +++ b/src/fdosecrets/objects/Prompt.h @@ -18,27 +18,41 @@ #ifndef KEEPASSXC_FDOSECRETS_PROMPT_H #define KEEPASSXC_FDOSECRETS_PROMPT_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/PromptAdaptor.h" +#include "core/Global.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" +#include #include class QWindow; class DatabaseWidget; +class Entry; namespace FdoSecrets { class Service; - class PromptBase : public DBusObjectHelper + class PromptBase : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT_LITERAL) public: - virtual DBusReturn prompt(const QString& windowId) = 0; + Q_INVOKABLE virtual DBusResult prompt(const DBusClientPtr& client, const QString& windowId) = 0; - virtual DBusReturn dismiss(); + Q_INVOKABLE virtual DBusResult dismiss(); + + template static PromptBase* Create(Service* parent, ARGS&&... args) + { + QScopedPointer res{new PROMPT(parent, std::forward(args)...)}; + if (!res->dbus()->registerObject(res.data())) { + // internal error; + return nullptr; + } + return res.take(); + } signals: void completed(bool dismissed, const QVariant& result); @@ -46,7 +60,6 @@ namespace FdoSecrets protected: explicit PromptBase(Service* parent); - bool registerSelf(); QWindow* findWindow(const QString& windowId); Service* service() const; }; @@ -60,11 +73,11 @@ namespace FdoSecrets explicit DeleteCollectionPrompt(Service* parent, Collection* coll); public: - static DBusReturn Create(Service* parent, Collection* coll); - - DBusReturn prompt(const QString& windowId) override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; private: + friend class PromptBase; + QPointer m_collection; }; @@ -72,16 +85,17 @@ namespace FdoSecrets { Q_OBJECT - explicit CreateCollectionPrompt(Service* parent); + explicit CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias); public: - static DBusReturn Create(Service* parent); + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + private: + friend class PromptBase; - signals: - void collectionCreated(Collection* coll); + QVariantMap m_properties; + QString m_alias; }; class LockCollectionsPrompt : public PromptBase @@ -91,35 +105,46 @@ namespace FdoSecrets explicit LockCollectionsPrompt(Service* parent, const QList& colls); public: - static DBusReturn Create(Service* parent, const QList& colls); - - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; private: + friend class PromptBase; + QList> m_collections; QList m_locked; }; - class UnlockCollectionsPrompt : public PromptBase + class DBusClient; + class UnlockPrompt : public PromptBase { Q_OBJECT - explicit UnlockCollectionsPrompt(Service* parent, const QList& coll); + explicit UnlockPrompt(Service* parent, const QSet& colls, const QSet& items); public: - static DBusReturn Create(Service* parent, const QList& coll); - - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; private slots: void collectionUnlockFinished(bool accepted); + void itemUnlockFinished(const QHash& results); private: + void unlockItems(); + + friend class PromptBase; + + static constexpr auto FdoSecretsBackend = "FdoSecretsBackend"; + QList> m_collections; + QHash>> m_items; QList m_unlocked; int m_numRejected = 0; + + // info about calling client + QWeakPointer m_client; + QString m_windowId; }; class Item; @@ -130,14 +155,46 @@ namespace FdoSecrets explicit DeleteItemPrompt(Service* parent, Item* item); public: - static DBusReturn Create(Service* parent, Item* item); - - DBusReturn prompt(const QString& windowId) override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; private: + friend class PromptBase; + QPointer m_item; }; + class CreateItemPrompt : public PromptBase + { + Q_OBJECT + + explicit CreateItemPrompt(Service* parent, + Collection* coll, + QVariantMap properties, + Secret secret, + QString itemPath, + Item* existing); + + public: + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; + private slots: + void itemUnlocked(bool dismissed, const QVariant& result); + + private: + DBusResult updateItem(); + + friend class PromptBase; + + QPointer m_coll; + QVariantMap m_properties; + Secret m_secret; + QString m_itemPath; + QPointer m_item; + + QPointer m_sess; + QWeakPointer m_client; + }; + } // namespace FdoSecrets #endif // KEEPASSXC_FDOSECRETS_PROMPT_H diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index 957203d8..38dc0aff 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -19,6 +19,7 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Prompt.h" @@ -28,7 +29,6 @@ #include "gui/DatabaseWidget.h" #include -#include #include #include @@ -39,9 +39,10 @@ namespace namespace FdoSecrets { - QSharedPointer Service::Create(FdoSecretsPlugin* plugin, QPointer dbTabs) + QSharedPointer + Service::Create(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus) { - QSharedPointer res{new Service(plugin, std::move(dbTabs))}; + QSharedPointer res{new Service(plugin, std::move(dbTabs), std::move(dbus))}; if (!res->initialize()) { return {}; } @@ -49,43 +50,25 @@ namespace FdoSecrets } Service::Service(FdoSecretsPlugin* plugin, - QPointer dbTabs) // clazy: exclude=ctor-missing-parent-argument - : DBusObjectHelper(nullptr) + QPointer dbTabs, + QSharedPointer dbus) // clazy: exclude=ctor-missing-parent-argument + : DBusObject(std::move(dbus)) , m_plugin(plugin) , m_databases(std::move(dbTabs)) , m_insideEnsureDefaultAlias(false) - , m_serviceWatcher(nullptr) { connect( m_databases, &DatabaseTabWidget::databaseUnlockDialogFinished, this, &Service::doneUnlockDatabaseInDialog); } - Service::~Service() - { - QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET)); - } + Service::~Service() = default; bool Service::initialize() { - if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) { - plugin()->emitError( - tr("Failed to register DBus service at %1.
").arg(QLatin1String(DBUS_SERVICE_SECRET)) - + m_plugin->reportExistingService()); + if (!dbus()->registerObject(this)) { return false; } - if (!registerWithPath(QStringLiteral(DBUS_PATH_SECRETS))) { - plugin()->emitError(tr("Failed to register DBus path %1.
").arg(QStringLiteral(DBUS_PATH_SECRETS))); - return false; - } - - // Connect to service unregistered signal - m_serviceWatcher.reset(new QDBusServiceWatcher()); - connect( - m_serviceWatcher.get(), &QDBusServiceWatcher::serviceUnregistered, this, &Service::dbusServiceUnregistered); - - m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); - // Add existing database tabs for (int idx = 0; idx != m_databases->count(); ++idx) { auto dbWidget = m_databases->databaseWidgetFromIndex(idx); @@ -199,161 +182,157 @@ namespace FdoSecrets m_insideEnsureDefaultAlias = false; } - void Service::dbusServiceUnregistered(const QString& service) + DBusResult Service::collections(QList& collections) const { - Q_ASSERT(m_serviceWatcher); - - auto removed = m_serviceWatcher->removeWatchedService(service); - if (!removed) { - qDebug("FdoSecrets: Failed to remove service watcher"); - } - - Session::CleanupNegotiation(service); - auto sess = m_peerToSession.value(service, nullptr); - if (sess) { - sess->close().okOrDie(); - } + collections = m_collections; + return {}; } - DBusReturn> Service::collections() const + DBusResult Service::openSession(const DBusClientPtr& client, + const QString& algorithm, + const QVariant& input, + QVariant& output, + Session*& result) { - return m_collections; - } - - DBusReturn Service::openSession(const QString& algorithm, const QVariant& input, Session*& result) - { - QVariant output; - bool incomplete = false; - auto peer = callingPeer(); - - // watch for service unregister to cleanup - Q_ASSERT(m_serviceWatcher); - m_serviceWatcher->addWatchedService(peer); - // negotiate cipher - auto ciphers = Session::CreateCiphers(peer, algorithm, input, output, incomplete); + bool incomplete = false; + auto ciphers = client->negotiateCipher(algorithm, input, output, incomplete); if (incomplete) { result = nullptr; - return output; + return {}; } if (!ciphers) { - return DBusReturn<>::Error(QDBusError::NotSupported); + return QDBusError::NotSupported; } - result = Session::Create(std::move(ciphers), callingPeerName(), this); + + // create session using the negotiated cipher + result = Session::Create(std::move(ciphers), client->name(), this); if (!result) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); + return QDBusError::InternalError; } - m_sessions.append(result); - m_peerToSession[peer] = result; - connect(result, &Session::aboutToClose, this, [this, peer, result]() { - emit sessionClosed(result); - m_sessions.removeAll(result); - m_peerToSession.remove(peer); + // remove session when the client disconnects + connect(dbus().data(), &DBusMgr::clientDisconnected, result, [result, client](const DBusClientPtr& toRemove) { + if (toRemove == client) { + result->close().okOrDie(); + } }); - emit sessionOpened(result); - return output; + // keep a list of sessions + m_sessions.append(result); + connect(result, &Session::aboutToClose, this, [this, result]() { m_sessions.removeAll(result); }); + + return {}; } - DBusReturn - Service::createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt) + DBusResult Service::createCollection(const QVariantMap& properties, + const QString& alias, + Collection*& collection, + PromptBase*& prompt) { prompt = nullptr; // return existing collection if alias is non-empty and exists. - auto collection = findCollection(alias); + collection = findCollection(alias); if (!collection) { - auto cp = CreateCollectionPrompt::Create(this); - if (cp.isError()) { - return cp; + prompt = PromptBase::Create(this, properties, alias); + if (!prompt) { + return QDBusError::InternalError; } - prompt = cp.value(); - - // collection will be created when the prompt completes. - // once it's done, we set additional properties on the collection - connect(cp.value(), - &CreateCollectionPrompt::collectionCreated, - cp.value(), - [alias, properties](Collection* coll) { - coll->setProperties(properties).okOrDie(); - if (!alias.isEmpty()) { - coll->addAlias(alias).okOrDie(); - } - }); } - return collection; + return {}; } - DBusReturn> Service::searchItems(const StringStringMap& attributes, QList& locked) + DBusResult Service::searchItems(const DBusClientPtr& client, + const StringStringMap& attributes, + QList& unlocked, + QList& locked) const { - auto ret = collections(); - if (ret.isError()) { + QList colls; + auto ret = collections(colls); + if (ret.err()) { return ret; } - QList unlocked; - for (const auto& coll : ret.value()) { - auto items = coll->searchItems(attributes); - if (items.isError()) { - return items; + for (const auto& coll : asConst(colls)) { + QList items; + ret = coll->searchItems(attributes, items); + if (ret.err()) { + return ret; } - auto l = coll->locked(); - if (l.isError()) { - return l; - } - if (l.value()) { - locked.append(items.value()); - } else { - unlocked.append(items.value()); - } - } - return unlocked; - } - - DBusReturn> Service::unlock(const QList& objects, PromptBase*& prompt) - { - QSet needUnlock; - needUnlock.reserve(objects.size()); - for (const auto& obj : asConst(objects)) { - auto coll = qobject_cast(obj); - if (coll) { - needUnlock << coll; - } else { - auto item = qobject_cast(obj); - if (!item) { - continue; + // item locked state already covers its collection's locked state + for (const auto& item : asConst(items)) { + bool l; + ret = item->locked(client, l); + if (ret.err()) { + return ret; + } + if (l) { + locked.append(item); + } else { + unlocked.append(item); } - // we lock the whole collection for item - needUnlock << item->collection(); } } - - // return anything already unlocked - QList unlocked; - QList toUnlock; - for (const auto& coll : asConst(needUnlock)) { - auto l = coll->locked(); - if (l.isError()) { - return l; - } - if (!l.value()) { - unlocked << coll; - } else { - toUnlock << coll; - } - } - if (!toUnlock.isEmpty()) { - auto up = UnlockCollectionsPrompt::Create(this, toUnlock); - if (up.isError()) { - return up; - } - prompt = up.value(); - } - return unlocked; + return {}; } - DBusReturn> Service::lock(const QList& objects, PromptBase*& prompt) + DBusResult Service::unlock(const DBusClientPtr& client, + const QList& objects, + QList& unlocked, + PromptBase*& prompt) + { + QSet collectionsToUnlock; + QSet itemsToUnlock; + collectionsToUnlock.reserve(objects.size()); + itemsToUnlock.reserve(objects.size()); + + for (const auto& obj : asConst(objects)) { + // the object is either an item or an collection + auto item = qobject_cast(obj); + auto coll = item ? item->collection() : qobject_cast(obj); + // either way there should be a collection + if (!coll) { + continue; + } + + bool collLocked{false}, itemLocked{false}; + // if the collection needs unlock + auto ret = coll->locked(collLocked); + if (ret.err()) { + return ret; + } + if (collLocked) { + collectionsToUnlock << coll; + } + + if (item) { + // item may also need unlock + ret = item->locked(client, itemLocked); + if (ret.err()) { + return ret; + } + if (itemLocked) { + itemsToUnlock << item; + } + } + + // both collection and item are not locked + if (!collLocked && !itemLocked) { + unlocked << obj; + } + } + + if (!collectionsToUnlock.isEmpty() || !itemsToUnlock.isEmpty()) { + prompt = PromptBase::Create(this, collectionsToUnlock, itemsToUnlock); + if (!prompt) { + return QDBusError::InternalError; + } + } + return {}; + } + + DBusResult Service::lock(const QList& objects, QList& locked, PromptBase*& prompt) { QSet needLock; needLock.reserve(objects.size()); @@ -372,64 +351,62 @@ namespace FdoSecrets } // return anything already locked - QList locked; QList toLock; for (const auto& coll : asConst(needLock)) { - auto l = coll->locked(); - if (l.isError()) { - return l; + bool l; + auto ret = coll->locked(l); + if (ret.err()) { + return ret; } - if (l.value()) { + if (l) { locked << coll; } else { toLock << coll; } } if (!toLock.isEmpty()) { - auto lp = LockCollectionsPrompt::Create(this, toLock); - if (lp.isError()) { - return lp; + prompt = PromptBase::Create(this, toLock); + if (!prompt) { + return QDBusError::InternalError; } - prompt = lp.value(); } - return locked; + return {}; } - DBusReturn> Service::getSecrets(const QList& items, Session* session) + DBusResult Service::getSecrets(const DBusClientPtr& client, + const QList& items, + Session* session, + ItemSecretMap& secrets) const { if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } - QHash res; - for (const auto& item : asConst(items)) { - auto ret = item->getSecret(session); - if (ret.isError()) { + auto ret = item->getSecretNoNotification(client, session, secrets[item]); + if (ret.err()) { return ret; } - res[item] = std::move(ret).value(); } - if (calledFromDBus()) { - plugin()->emitRequestShowNotification( - tr(R"(%n Entry(s) was used by %1)", "%1 is the name of an application", res.size()) - .arg(callingPeerName())); - } - return res; + plugin()->emitRequestShowNotification( + tr(R"(%n Entry(s) was used by %1)", "%1 is the name of an application", secrets.size()) + .arg(client->name())); + return {}; } - DBusReturn Service::readAlias(const QString& name) + DBusResult Service::readAlias(const QString& name, Collection*& collection) const { - return findCollection(name); + collection = findCollection(name); + return {}; } - DBusReturn Service::setAlias(const QString& name, Collection* collection) + DBusResult Service::setAlias(const QString& name, Collection* collection) { if (!collection) { // remove alias name from its collection collection = findCollection(name); if (!collection) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return collection->removeAlias(name); } @@ -481,7 +458,7 @@ namespace FdoSecrets return m_dbToCollection.value(db, nullptr); } - const QList Service::sessions() const + QList Service::sessions() const { return m_sessions; } diff --git a/src/fdosecrets/objects/Service.h b/src/fdosecrets/objects/Service.h index 5b1ff5ac..674e5c22 100644 --- a/src/fdosecrets/objects/Service.h +++ b/src/fdosecrets/objects/Service.h @@ -18,18 +18,14 @@ #ifndef KEEPASSXC_FDOSECRETS_SERVICE_H #define KEEPASSXC_FDOSECRETS_SERVICE_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/ServiceAdaptor.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" #include #include #include #include -#include - -class QDBusServiceWatcher; - class DatabaseTabWidget; class DatabaseWidget; class Group; @@ -42,14 +38,14 @@ namespace FdoSecrets class Collection; class Item; class PromptBase; - class ServiceAdaptor; class Session; - class Service : public DBusObjectHelper // clazy: exclude=ctor-missing-parent-argument + class Service : public DBusObject // clazy: exclude=ctor-missing-parent-argument { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SERVICE_LITERAL) - explicit Service(FdoSecretsPlugin* plugin, QPointer dbTabs); + explicit Service(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus); public: /** @@ -58,38 +54,51 @@ namespace FdoSecrets * This may be caused by * - failed initialization */ - static QSharedPointer Create(FdoSecretsPlugin* plugin, QPointer dbTabs); + static QSharedPointer + Create(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus); ~Service() override; - DBusReturn openSession(const QString& algorithm, const QVariant& input, Session*& result); - DBusReturn - createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt); - DBusReturn> searchItems(const StringStringMap& attributes, QList& locked); + Q_INVOKABLE DBusResult openSession(const DBusClientPtr& client, + const QString& algorithm, + const QVariant& input, + QVariant& output, + Session*& result); + Q_INVOKABLE DBusResult createCollection(const QVariantMap& properties, + const QString& alias, + Collection*& collection, + PromptBase*& prompt); + Q_INVOKABLE DBusResult searchItems(const DBusClientPtr& client, + const StringStringMap& attributes, + QList& unlocked, + QList& locked) const; - DBusReturn> unlock(const QList& objects, PromptBase*& prompt); + Q_INVOKABLE DBusResult unlock(const DBusClientPtr& client, + const QList& objects, + QList& unlocked, + PromptBase*& prompt); - DBusReturn> lock(const QList& objects, PromptBase*& prompt); + Q_INVOKABLE DBusResult lock(const QList& objects, QList& locked, PromptBase*& prompt); - DBusReturn> getSecrets(const QList& items, Session* session); + Q_INVOKABLE DBusResult getSecrets(const DBusClientPtr& client, + const QList& items, + Session* session, + ItemSecretMap& secrets) const; - DBusReturn readAlias(const QString& name); + Q_INVOKABLE DBusResult readAlias(const QString& name, Collection*& collection) const; - DBusReturn setAlias(const QString& name, Collection* collection); + Q_INVOKABLE DBusResult setAlias(const QString& name, Collection* collection); /** * List of collections * @return */ - DBusReturn> collections() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult collections(QList& collections) const; signals: void collectionCreated(Collection* collection); void collectionDeleted(Collection* collection); void collectionChanged(Collection* collection); - void sessionOpened(Session* sess); - void sessionClosed(Session* sess); - /** * Finish signal for async action doUnlockDatabaseInDialog * @param accepted If false, the action is canceled by the user @@ -102,7 +111,7 @@ namespace FdoSecrets * List of sessions * @return */ - const QList sessions() const; + QList sessions() const; FdoSecretsPlugin* plugin() const { @@ -121,7 +130,6 @@ namespace FdoSecrets void doUnlockDatabaseInDialog(DatabaseWidget* dbWidget); private slots: - void dbusServiceUnregistered(const QString& service); void ensureDefaultAlias(); void onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal); @@ -158,11 +166,8 @@ namespace FdoSecrets QHash m_dbToCollection; QList m_sessions; - QHash m_peerToSession; bool m_insideEnsureDefaultAlias; - - std::unique_ptr m_serviceWatcher; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Session.cpp b/src/fdosecrets/objects/Session.cpp index 0c643f2f..04c9f607 100644 --- a/src/fdosecrets/objects/Session.cpp +++ b/src/fdosecrets/objects/Session.cpp @@ -14,53 +14,36 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "Session.h" #include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/SessionCipher.h" #include "core/Tools.h" namespace FdoSecrets { - - QHash Session::negotiationState; - - Session* Session::Create(std::unique_ptr&& cipher, const QString& peer, Service* parent) + Session* Session::Create(QSharedPointer cipher, const QString& peer, Service* parent) { QScopedPointer res{new Session(std::move(cipher), peer, parent)}; - - if (!res->registerSelf()) { + if (!res->dbus()->registerObject(res.data())) { return nullptr; } return res.take(); } - Session::Session(std::unique_ptr&& cipher, const QString& peer, Service* parent) - : DBusObjectHelper(parent) + Session::Session(QSharedPointer cipher, const QString& peer, Service* parent) + : DBusObject(parent) , m_cipher(std::move(cipher)) , m_peer(peer) , m_id(QUuid::createUuid()) { } - bool Session::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_SESSION).arg(p()->objectPath().path(), id()); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register session on DBus at path '%1'").arg(path)); - } - return ok; - } - - void Session::CleanupNegotiation(const QString& peer) - { - negotiationState.remove(peer); - } - - DBusReturn Session::close() + DBusResult Session::close() { emit aboutToClose(); deleteLater(); @@ -83,48 +66,16 @@ namespace FdoSecrets return qobject_cast(parent()); } - std::unique_ptr Session::CreateCiphers(const QString& peer, - const QString& algorithm, - const QVariant& input, - QVariant& output, - bool& incomplete) - { - Q_UNUSED(peer); - incomplete = false; - - std::unique_ptr cipher{}; - if (algorithm == QLatin1String(PlainCipher::Algorithm)) { - cipher.reset(new PlainCipher); - } else if (algorithm == QLatin1String(DhIetf1024Sha256Aes128CbcPkcs7::Algorithm)) { - QByteArray clientPublicKey = input.toByteArray(); - cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey)); - } else { - // error notSupported - } - - if (!cipher) { - return {}; - } - - if (!cipher->isValid()) { - qWarning() << "FdoSecrets: Error creating cipher"; - return {}; - } - - output = cipher->negotiationOutput(); - return cipher; - } - - SecretStruct Session::encode(const SecretStruct& input) const + Secret Session::encode(const Secret& input) const { auto output = m_cipher->encrypt(input); - output.session = objectPath(); + output.session = this; return output; } - SecretStruct Session::decode(const SecretStruct& input) const + Secret Session::decode(const Secret& input) const { + Q_ASSERT(input.session == this); return m_cipher->decrypt(input); } - } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Session.h b/src/fdosecrets/objects/Session.h index 3bb6ea25..f3366d68 100644 --- a/src/fdosecrets/objects/Session.h +++ b/src/fdosecrets/objects/Session.h @@ -18,36 +18,24 @@ #ifndef KEEPASSXC_FDOSECRETS_SESSION_H #define KEEPASSXC_FDOSECRETS_SESSION_H -#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/dbus/DBusObject.h" #include "fdosecrets/objects/Service.h" -#include "fdosecrets/objects/SessionCipher.h" -#include "fdosecrets/objects/adaptors/SessionAdaptor.h" -#include -#include +#include #include #include -#include - namespace FdoSecrets { - class CipherPair; - class Session : public DBusObjectHelper + class Session : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SESSION_LITERAL) - explicit Session(std::unique_ptr&& cipher, const QString& peer, Service* parent); + explicit Session(QSharedPointer cipher, const QString& peer, Service* parent); public: - static std::unique_ptr CreateCiphers(const QString& peer, - const QString& algorithm, - const QVariant& input, - QVariant& output, - bool& incomplete); - static void CleanupNegotiation(const QString& peer); - /** * @brief Create a new instance of `Session`. * @param cipher the negotiated cipher @@ -57,23 +45,23 @@ namespace FdoSecrets * This may be caused by * - DBus path registration error */ - static Session* Create(std::unique_ptr&& cipher, const QString& peer, Service* parent); + static Session* Create(QSharedPointer cipher, const QString& peer, Service* parent); - DBusReturn close(); + Q_INVOKABLE DBusResult close(); /** * Encode the secret struct. Note only the value field is encoded. * @param input * @return */ - SecretStruct encode(const SecretStruct& input) const; + Secret encode(const Secret& input) const; /** * Decode the secret struct. * @param input * @return */ - SecretStruct decode(const SecretStruct& input) const; + Secret decode(const Secret& input) const; /** * The peer application that opened this session @@ -93,14 +81,9 @@ namespace FdoSecrets void aboutToClose(); private: - bool registerSelf(); - - private: - std::unique_ptr m_cipher; + QSharedPointer m_cipher; QString m_peer; QUuid m_id; - - static QHash negotiationState; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/SessionCipher.cpp b/src/fdosecrets/objects/SessionCipher.cpp index 26f080c3..efb6da1f 100644 --- a/src/fdosecrets/objects/SessionCipher.cpp +++ b/src/fdosecrets/objects/SessionCipher.cpp @@ -149,9 +149,9 @@ namespace FdoSecrets return OKM; } - SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const SecretStruct& input) + Secret DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const Secret& input) { - SecretStruct output = input; + Secret output = input; output.value.clear(); output.parameters.clear(); @@ -187,7 +187,7 @@ namespace FdoSecrets return input; } - SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const SecretStruct& input) + Secret DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const Secret& input) { auto IV = input.parameters; SymmetricCipher decrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); @@ -196,7 +196,7 @@ namespace FdoSecrets return input; } bool ok; - SecretStruct output = input; + Secret output = input; output.parameters.clear(); output.value = decrypter.process(input.value, &ok); diff --git a/src/fdosecrets/objects/SessionCipher.h b/src/fdosecrets/objects/SessionCipher.h index 4d656c0a..e1450784 100644 --- a/src/fdosecrets/objects/SessionCipher.h +++ b/src/fdosecrets/objects/SessionCipher.h @@ -33,8 +33,8 @@ namespace FdoSecrets public: CipherPair() = default; virtual ~CipherPair() = default; - virtual SecretStruct encrypt(const SecretStruct& input) = 0; - virtual SecretStruct decrypt(const SecretStruct& input) = 0; + virtual Secret encrypt(const Secret& input) = 0; + virtual Secret decrypt(const Secret& input) = 0; virtual bool isValid() const = 0; virtual QVariant negotiationOutput() const = 0; }; @@ -46,12 +46,12 @@ namespace FdoSecrets static constexpr const char Algorithm[] = "plain"; PlainCipher() = default; - SecretStruct encrypt(const SecretStruct& input) override + Secret encrypt(const Secret& input) override { return input; } - SecretStruct decrypt(const SecretStruct& input) override + Secret decrypt(const Secret& input) override { return input; } @@ -120,9 +120,9 @@ namespace FdoSecrets explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes); - SecretStruct encrypt(const SecretStruct& input) override; + Secret encrypt(const Secret& input) override; - SecretStruct decrypt(const SecretStruct& input) override; + Secret decrypt(const Secret& input) override; bool isValid() const override; diff --git a/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp b/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp deleted file mode 100644 index 275145b4..00000000 --- a/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "CollectionAdaptor.h" - -#include "fdosecrets/objects/Collection.h" -#include "fdosecrets/objects/Item.h" -#include "fdosecrets/objects/Prompt.h" - -namespace FdoSecrets -{ - - CollectionAdaptor::CollectionAdaptor(Collection* parent) - : DBusAdaptor(parent) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &Collection::itemCreated, this, [this](const Item* item) { - emit ItemCreated(objectPathSafe(item)); - }); - connect(parent, &Collection::itemDeleted, this, [this](const Item* item) { - emit ItemDeleted(objectPathSafe(item)); - }); - connect(parent, &Collection::itemChanged, this, [this](const Item* item) { - emit ItemChanged(objectPathSafe(item)); - }); - } - - const QList CollectionAdaptor::items() const - { - return objectsToPath(p()->items().valueOrHandle(p())); - } - - QString CollectionAdaptor::label() const - { - return p()->label().valueOrHandle(p()); - } - - void CollectionAdaptor::setLabel(const QString& label) - { - p()->setLabel(label).handle(p()); - } - - bool CollectionAdaptor::locked() const - { - return p()->locked().valueOrHandle(p()); - } - - qulonglong CollectionAdaptor::created() const - { - return p()->created().valueOrHandle(p()); - } - - qulonglong CollectionAdaptor::modified() const - { - return p()->modified().valueOrHandle(p()); - } - - QDBusObjectPath CollectionAdaptor::Delete() - { - return objectPathSafe(p()->deleteCollection().valueOrHandle(p())); - } - - QList CollectionAdaptor::SearchItems(const StringStringMap& attributes) - { - return objectsToPath(p()->searchItems(attributes).valueOrHandle(p())); - } - - QDBusObjectPath CollectionAdaptor::CreateItem(const QVariantMap& properties, - const SecretStruct& secret, - bool replace, - QDBusObjectPath& prompt) - { - PromptBase* pp = nullptr; - auto item = p()->createItem(properties, secret, replace, pp).valueOrHandle(p()); - prompt = objectPathSafe(pp); - return objectPathSafe(item); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/CollectionAdaptor.h b/src/fdosecrets/objects/adaptors/CollectionAdaptor.h deleted file mode 100644 index f5220108..00000000 --- a/src/fdosecrets/objects/adaptors/CollectionAdaptor.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H -#define KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H - -#include "fdosecrets/objects/adaptors/DBusAdaptor.h" - -#include - -namespace FdoSecrets -{ - - class Collection; - class CollectionAdaptor : public DBusAdaptor - { - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_COLLECTION) - - Q_PROPERTY(QList Items READ items) - Q_PROPERTY(QString Label READ label WRITE setLabel) - Q_PROPERTY(bool Locked READ locked) - Q_PROPERTY(qulonglong Created READ created) - Q_PROPERTY(qulonglong Modified READ modified) - - public: - explicit CollectionAdaptor(Collection* parent); - ~CollectionAdaptor() override = default; - - const QList items() const; - - QString label() const; - void setLabel(const QString& label); - - bool locked() const; - - qulonglong created() const; - - qulonglong modified() const; - - public slots: - QDBusObjectPath Delete(); - QList SearchItems(const StringStringMap& attributes); - QDBusObjectPath CreateItem(const QVariantMap& properties, - const FdoSecrets::SecretStruct& secret, - bool replace, - QDBusObjectPath& prompt); - - signals: - void ItemCreated(const QDBusObjectPath& item); - void ItemDeleted(const QDBusObjectPath& item); - void ItemChanged(const QDBusObjectPath& item); - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/DBusAdaptor.h b/src/fdosecrets/objects/adaptors/DBusAdaptor.h deleted file mode 100644 index 93bbc72f..00000000 --- a/src/fdosecrets/objects/adaptors/DBusAdaptor.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H -#define KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H - -#include "fdosecrets/objects/DBusReturn.h" -#include "fdosecrets/objects/DBusTypes.h" - -#include - -namespace FdoSecrets -{ - - /** - * @brief A common adapter class - */ - template class DBusAdaptor : public QDBusAbstractAdaptor - { - public: - explicit DBusAdaptor(Parent* parent = nullptr) - : QDBusAbstractAdaptor(parent) - { - } - - ~DBusAdaptor() override = default; - - protected: - Parent* p() const - { - return qobject_cast(parent()); - } - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp b/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp deleted file mode 100644 index 7116041b..00000000 --- a/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "ItemAdaptor.h" - -#include "fdosecrets/objects/Item.h" -#include "fdosecrets/objects/Prompt.h" -#include "fdosecrets/objects/Session.h" - -namespace FdoSecrets -{ - - ItemAdaptor::ItemAdaptor(Item* parent) - : DBusAdaptor(parent) - { - } - - bool ItemAdaptor::locked() const - { - return p()->locked().valueOrHandle(p()); - } - - const StringStringMap ItemAdaptor::attributes() const - { - return p()->attributes().valueOrHandle(p()); - } - - void ItemAdaptor::setAttributes(const StringStringMap& attrs) - { - p()->setAttributes(attrs).handle(p()); - } - - QString ItemAdaptor::label() const - { - return p()->label().valueOrHandle(p()); - } - - void ItemAdaptor::setLabel(const QString& label) - { - p()->setLabel(label).handle(p()); - } - - qulonglong ItemAdaptor::created() const - { - return p()->created().valueOrHandle(p()); - } - - qulonglong ItemAdaptor::modified() const - { - return p()->modified().valueOrHandle(p()); - } - - QDBusObjectPath ItemAdaptor::Delete() - { - auto prompt = p()->deleteItem().valueOrHandle(p()); - return objectPathSafe(prompt); - } - - SecretStruct ItemAdaptor::GetSecret(const QDBusObjectPath& session) - { - return p()->getSecret(pathToObject(session)).valueOrHandle(p()); - } - - void ItemAdaptor::SetSecret(const SecretStruct& secret) - { - p()->setSecret(secret).handle(p()); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/ItemAdaptor.h b/src/fdosecrets/objects/adaptors/ItemAdaptor.h deleted file mode 100644 index 4a6da4bf..00000000 --- a/src/fdosecrets/objects/adaptors/ItemAdaptor.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H -#define KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H - -#include "fdosecrets/objects/adaptors/DBusAdaptor.h" - -namespace FdoSecrets -{ - - class Item; - class ItemAdaptor : public DBusAdaptor - { - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_ITEM) - - Q_PROPERTY(bool Locked READ locked) - Q_PROPERTY(StringStringMap Attributes READ attributes WRITE setAttributes) - Q_PROPERTY(QString Label READ label WRITE setLabel) - Q_PROPERTY(qulonglong Created READ created) - Q_PROPERTY(qulonglong Modified READ modified) - - public: - explicit ItemAdaptor(Item* parent); - ~ItemAdaptor() override = default; - - bool locked() const; - - const StringStringMap attributes() const; - void setAttributes(const StringStringMap& attrs); - - QString label() const; - void setLabel(const QString& label); - - qulonglong created() const; - - qulonglong modified() const; - - public slots: - QDBusObjectPath Delete(); - FdoSecrets::SecretStruct GetSecret(const QDBusObjectPath& session); - void SetSecret(const FdoSecrets::SecretStruct& secret); - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp b/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp deleted file mode 100644 index ff8a945c..00000000 --- a/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "PromptAdaptor.h" - -#include "fdosecrets/objects/Prompt.h" - -namespace FdoSecrets -{ - - PromptAdaptor::PromptAdaptor(PromptBase* parent) - : DBusAdaptor(parent) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &PromptBase::completed, this, [this](bool dismissed, QVariant result) { - // make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it. - if (!result.isValid()) { - result = QString{}; - } - emit Completed(dismissed, QDBusVariant(std::move(result))); - }); - } - - void PromptAdaptor::Prompt(const QString& windowId) - { - p()->prompt(windowId).handle(p()); - } - - void PromptAdaptor::Dismiss() - { - p()->dismiss().handle(p()); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/PromptAdaptor.h b/src/fdosecrets/objects/adaptors/PromptAdaptor.h deleted file mode 100644 index 9f439081..00000000 --- a/src/fdosecrets/objects/adaptors/PromptAdaptor.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H -#define KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H - -#include "fdosecrets/objects/adaptors/DBusAdaptor.h" - -namespace FdoSecrets -{ - - class PromptBase; - class PromptAdaptor : public DBusAdaptor - { - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT) - - public: - explicit PromptAdaptor(PromptBase* parent); - ~PromptAdaptor() override = default; - - public slots: - void Prompt(const QString& windowId); - void Dismiss(); - - signals: - void Completed(bool dismissed, const QDBusVariant& result); - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp b/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp deleted file mode 100644 index cacf9a99..00000000 --- a/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "ServiceAdaptor.h" - -#include "fdosecrets/objects/Collection.h" -#include "fdosecrets/objects/Item.h" -#include "fdosecrets/objects/Prompt.h" -#include "fdosecrets/objects/Service.h" -#include "fdosecrets/objects/Session.h" - -namespace FdoSecrets -{ - - ServiceAdaptor::ServiceAdaptor(Service* parent) - : DBusAdaptor(parent) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &Service::collectionCreated, this, [this](Collection* coll) { - emit CollectionCreated(objectPathSafe(coll)); - }); - connect(parent, &Service::collectionDeleted, this, [this](Collection* coll) { - emit CollectionDeleted(objectPathSafe(coll)); - }); - connect(parent, &Service::collectionChanged, this, [this](Collection* coll) { - emit CollectionChanged(objectPathSafe(coll)); - }); - } - - const QList ServiceAdaptor::collections() const - { - auto colls = p()->collections().valueOrHandle(p()); - return objectsToPath(std::move(colls)); - } - - QDBusVariant - ServiceAdaptor::OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result) - { - Session* session = nullptr; - auto output = p()->openSession(algorithm, input.variant(), session).valueOrHandle(p()); - result = objectPathSafe(session); - return QDBusVariant(std::move(output)); - } - - QDBusObjectPath - ServiceAdaptor::CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt) - { - PromptBase* pp; - auto coll = p()->createCollection(properties, alias, pp).valueOrHandle(p()); - prompt = objectPathSafe(pp); - return objectPathSafe(coll); - } - - const QList ServiceAdaptor::SearchItems(const StringStringMap& attributes, - QList& locked) - { - QList lockedItems, unlockedItems; - unlockedItems = p()->searchItems(attributes, lockedItems).valueOrHandle(p()); - locked = objectsToPath(lockedItems); - return objectsToPath(unlockedItems); - } - - const QList ServiceAdaptor::Unlock(const QList& paths, QDBusObjectPath& prompt) - { - auto objects = pathsToObject(paths); - if (!paths.isEmpty() && objects.isEmpty()) { - DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); - return {}; - } - - PromptBase* pp = nullptr; - auto unlocked = p()->unlock(objects, pp).valueOrHandle(p()); - - prompt = objectPathSafe(pp); - return objectsToPath(unlocked); - } - - const QList ServiceAdaptor::Lock(const QList& paths, QDBusObjectPath& prompt) - { - auto objects = pathsToObject(paths); - if (!paths.isEmpty() && objects.isEmpty()) { - DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); - return {}; - } - - PromptBase* pp = nullptr; - auto locked = p()->lock(objects, pp).valueOrHandle(p()); - - prompt = objectPathSafe(pp); - return objectsToPath(locked); - } - - const ObjectPathSecretMap ServiceAdaptor::GetSecrets(const QList& items, - const QDBusObjectPath& session) - { - auto itemObjects = pathsToObject(items); - if (!items.isEmpty() && itemObjects.isEmpty()) { - DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); - return {}; - } - - auto secrets = p()->getSecrets(pathsToObject(items), pathToObject(session)).valueOrHandle(p()); - - ObjectPathSecretMap res; - auto iter = secrets.begin(); - while (iter != secrets.end()) { - res[objectPathSafe(iter.key())] = std::move(iter.value()); - ++iter; - } - return res; - } - - QDBusObjectPath ServiceAdaptor::ReadAlias(const QString& name) - { - auto coll = p()->readAlias(name).valueOrHandle(p()); - return objectPathSafe(coll); - } - - void ServiceAdaptor::SetAlias(const QString& name, const QDBusObjectPath& collection) - { - p()->setAlias(name, pathToObject(collection)).handle(p()); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/ServiceAdaptor.h b/src/fdosecrets/objects/adaptors/ServiceAdaptor.h deleted file mode 100644 index b369c127..00000000 --- a/src/fdosecrets/objects/adaptors/ServiceAdaptor.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2018 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H -#define KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H - -#include "DBusAdaptor.h" - -#include - -namespace FdoSecrets -{ - /** - * @brief Adapter class for interface org.freedesktop.Secret.Service - */ - class Service; - class ServiceAdaptor : public DBusAdaptor - { - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SERVICE) - - Q_PROPERTY(QList Collections READ collections) - - public: - explicit ServiceAdaptor(Service* parent); - ~ServiceAdaptor() override = default; - - const QList collections() const; - - public slots: - QDBusVariant OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result); - - QDBusObjectPath CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt); - - const QList SearchItems(const StringStringMap& attributes, QList& locked); - - const QList Unlock(const QList& paths, QDBusObjectPath& prompt); - - const QList Lock(const QList& paths, QDBusObjectPath& prompt); - - const ObjectPathSecretMap GetSecrets(const QList& items, const QDBusObjectPath& session); - - QDBusObjectPath ReadAlias(const QString& name); - - void SetAlias(const QString& name, const QDBusObjectPath& collection); - - signals: - void CollectionCreated(const QDBusObjectPath& collection); - - void CollectionDeleted(const QDBusObjectPath& collection); - - void CollectionChanged(const QDBusObjectPath& collection); - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H diff --git a/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp b/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp deleted file mode 100644 index 6597bfff..00000000 --- a/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "SessionAdaptor.h" - -#include "fdosecrets/objects/Session.h" - -namespace FdoSecrets -{ - - SessionAdaptor::SessionAdaptor(Session* parent) - : DBusAdaptor(parent) - { - } - - void SessionAdaptor::Close() - { - p()->close().handle(p()); - } - -} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/SessionAdaptor.h b/src/fdosecrets/objects/adaptors/SessionAdaptor.h deleted file mode 100644 index 40806170..00000000 --- a/src/fdosecrets/objects/adaptors/SessionAdaptor.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2019 Aetf - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H -#define KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H - -#include "fdosecrets/objects/adaptors/DBusAdaptor.h" - -namespace FdoSecrets -{ - - class Session; - class SessionAdaptor : public DBusAdaptor - { - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SESSION) - - public: - explicit SessionAdaptor(Session* parent); - ~SessionAdaptor() override = default; - - public slots: - void Close(); - }; - -} // namespace FdoSecrets - -#endif // KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H diff --git a/src/fdosecrets/widgets/AccessControlDialog.cpp b/src/fdosecrets/widgets/AccessControlDialog.cpp new file mode 100644 index 00000000..1cc3a983 --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2013 Francois Ferrand + * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "AccessControlDialog.h" +#include "ui_AccessControlDialog.h" + +#include "fdosecrets/widgets/RowButtonHelper.h" + +#include "core/Entry.h" + +#include + +#include + +AccessControlDialog::AccessControlDialog(QWindow* parent, + const QList& entries, + const QString& app, + AuthOptions authOptions) + : m_ui(new Ui::AccessControlDialog()) + , m_model(new EntryModel(entries)) +{ + if (parent) { + // Force the creation of the QWindow, without this windowHandle() will return nullptr + winId(); + auto window = windowHandle(); + Q_ASSERT(window); + window->setTransientParent(parent); + } + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + + m_ui->setupUi(this); + + connect(m_ui->cancelButton, &QPushButton::clicked, this, [this]() { done(DenyAll); }); + connect(m_ui->allowButton, &QPushButton::clicked, this, [this]() { done(AllowSelected); }); + connect(m_ui->itemsTable, &QTableView::clicked, m_model.data(), &EntryModel::toggleCheckState); + connect(m_ui->rememberCheck, &QCheckBox::clicked, this, &AccessControlDialog::rememberChecked); + connect(this, &QDialog::finished, this, &AccessControlDialog::dialogFinished); + + m_ui->rememberMsg->setCloseButtonVisible(false); + m_ui->rememberMsg->setMessageType(MessageWidget::Information); + + m_ui->appLabel->setText(m_ui->appLabel->text().arg(app)); + + m_ui->itemsTable->setModel(m_model.data()); + installWidgetItemDelegate(m_ui->itemsTable, 2, [this](QWidget* p, const QModelIndex& idx) { + auto btn = new DenyButton(p, idx); + connect(btn, &DenyButton::clicked, this, &AccessControlDialog::denyEntryClicked); + return btn; + }); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_ui->itemsTable->resizeColumnsToContents(); + + if (!authOptions.testFlag(AuthOption::Remember)) { + m_ui->rememberCheck->setHidden(true); + m_ui->rememberCheck->setChecked(false); + } + if (!authOptions.testFlag(AuthOption::PerEntryDeny)) { + m_ui->itemsTable->horizontalHeader()->setSectionHidden(2, true); + } + + m_ui->allowButton->setFocus(); +} + +AccessControlDialog::~AccessControlDialog() = default; + +void AccessControlDialog::rememberChecked(bool checked) +{ + if (checked) { + m_ui->rememberMsg->animatedShow(); + } else { + m_ui->rememberMsg->animatedHide(); + } +} + +void AccessControlDialog::denyEntryClicked(Entry* entry, const QModelIndex& index) +{ + m_decisions.insert(entry, AuthDecision::Denied); + m_model->removeRow(index.row()); + if (m_model->rowCount({}) == 0) { + reject(); + } +} + +void AccessControlDialog::dialogFinished(int result) +{ + auto allow = m_ui->rememberCheck->isChecked() ? AuthDecision::Allowed : AuthDecision::AllowedOnce; + auto deny = m_ui->rememberCheck->isChecked() ? AuthDecision::Denied : AuthDecision::DeniedOnce; + + for (int row = 0; row != m_model->rowCount({}); ++row) { + auto entry = m_model->data(m_model->index(row, 2), Qt::EditRole).value(); + auto selected = m_model->data(m_model->index(row, 0), Qt::CheckStateRole).value(); + Q_ASSERT(entry); + switch (result) { + case AllowSelected: + if (selected) { + m_decisions.insert(entry, allow); + } else { + m_decisions.insert(entry, AuthDecision::Undecided); + } + break; + case DenyAll: + m_decisions.insert(entry, deny); + break; + case Rejected: + default: + m_decisions.insert(entry, AuthDecision::Undecided); + break; + } + } + + emit finished(m_decisions); +} + +QHash AccessControlDialog::decisions() const +{ + return m_decisions; +} + +AccessControlDialog::EntryModel::EntryModel(QList entries, QObject* parent) + : QAbstractTableModel(parent) + , m_entries(std::move(entries)) + , m_selected(QSet::fromList(m_entries)) +{ +} + +int AccessControlDialog::EntryModel::rowCount(const QModelIndex& parent) const +{ + return isValid(parent) ? 0 : m_entries.count(); +} + +int AccessControlDialog::EntryModel::columnCount(const QModelIndex& parent) const +{ + return isValid(parent) ? 0 : 3; +} + +bool AccessControlDialog::EntryModel::isValid(const QModelIndex& index) const +{ + return index.isValid() && index.row() < rowCount({}) && index.column() < columnCount({}); +} + +void AccessControlDialog::EntryModel::toggleCheckState(const QModelIndex& index) +{ + if (!isValid(index)) { + return; + } + auto entry = m_entries.at(index.row()); + // click anywhere in the row to check/uncheck the item + auto it = m_selected.find(entry); + if (it == m_selected.end()) { + m_selected.insert(entry); + } else { + m_selected.erase(it); + } + auto rowIdx = index.sibling(index.row(), 0); + emit dataChanged(rowIdx, rowIdx, {Qt::CheckStateRole}); +} + +QVariant AccessControlDialog::EntryModel::data(const QModelIndex& index, int role) const +{ + if (!isValid(index)) { + return {}; + } + auto entry = m_entries.at(index.row()); + + switch (index.column()) { + case 0: + switch (role) { + case Qt::DisplayRole: + return entry->title(); + case Qt::DecorationRole: + return entry->icon(); + case Qt::CheckStateRole: + return QVariant::fromValue(m_selected.contains(entry) ? Qt::Checked : Qt::Unchecked); + default: + return {}; + } + case 1: + switch (role) { + case Qt::DisplayRole: + return entry->username(); + default: + return {}; + } + case 2: + switch (role) { + case Qt::EditRole: + return QVariant::fromValue(entry); + default: + return {}; + } + default: + return {}; + } +} + +bool AccessControlDialog::EntryModel::removeRows(int row, int count, const QModelIndex& parent) +{ + beginRemoveRows(parent, row, row + count - 1); + while (count--) { + m_entries.removeAt(row); + } + endRemoveRows(); + return true; +} + +AccessControlDialog::DenyButton::DenyButton(QWidget* p, const QModelIndex& idx) + : QPushButton(p) + , m_index(idx) + , m_entry() +{ + setText(tr("Deny for this program")); + connect(this, &QPushButton::clicked, [this]() { emit clicked(entry(), m_index); }); +} + +void AccessControlDialog::DenyButton::setEntry(Entry* e) +{ + m_entry = e; +} + +Entry* AccessControlDialog::DenyButton::entry() const +{ + return m_entry; +} diff --git a/src/fdosecrets/widgets/AccessControlDialog.h b/src/fdosecrets/widgets/AccessControlDialog.h new file mode 100644 index 00000000..0c394b12 --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2013 Francois Ferrand + * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_ACCESSCONTROLDIALOG_H +#define KEEPASSXC_FDOSECRETS_ACCESSCONTROLDIALOG_H + +#include +#include +#include +#include +#include +#include + +#include "core/Global.h" + +class Entry; + +namespace Ui +{ + class AccessControlDialog; +} + +enum class AuthOption +{ + None = 0, + Remember = 1 << 1, + PerEntryDeny = 1 << 2, +}; +Q_DECLARE_FLAGS(AuthOptions, AuthOption); +Q_DECLARE_OPERATORS_FOR_FLAGS(AuthOptions); + +class AccessControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AccessControlDialog(QWindow* parent, + const QList& entries, + const QString& app, + AuthOptions authOptions = AuthOption::Remember | AuthOption::PerEntryDeny); + ~AccessControlDialog() override; + + enum DialogCode + { + Rejected, + AllowSelected, + DenyAll, + }; + + QHash decisions() const; + +signals: + void finished(const QHash& results); + +private slots: + void rememberChecked(bool checked); + void denyEntryClicked(Entry* entry, const QModelIndex& index); + void dialogFinished(int result); + +private: + class EntryModel; + class DenyButton; + + QScopedPointer m_ui; + QScopedPointer m_model; + QHash m_decisions; +}; + +class AccessControlDialog::EntryModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit EntryModel(QList entries, QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool removeRows(int row, int count, const QModelIndex& parent) override; + +public slots: + void toggleCheckState(const QModelIndex& index); + +private: + bool isValid(const QModelIndex& index) const; + + QList m_entries; + QSet m_selected; +}; + +class AccessControlDialog::DenyButton : public QPushButton +{ + Q_OBJECT + + Q_PROPERTY(Entry* entry READ entry WRITE setEntry USER true) + + QPersistentModelIndex m_index; + QPointer m_entry; + +public: + explicit DenyButton(QWidget* p, const QModelIndex& idx); + + void setEntry(Entry* e); + Entry* entry() const; + +signals: + void clicked(Entry*, const QModelIndex& idx); +}; + +#endif // KEEPASSXC_FDOSECRETS_ACCESSCONTROLDIALOG_H diff --git a/src/fdosecrets/widgets/AccessControlDialog.ui b/src/fdosecrets/widgets/AccessControlDialog.ui new file mode 100644 index 00000000..2de17d5b --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.ui @@ -0,0 +1,133 @@ + + + AccessControlDialog + + + + 0 + 0 + 405 + 252 + + + + KeePassXC - Access Request + + + + + + + 50 + false + + + + <html><head/><body><p><span style=" font-weight:600;">%1 </span>is requesting access to the following entries:</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + Your decision for above entries will be remembered for the duration the requesting client is running. + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remember + + + true + + + + + + + Allow access to entries + + + Allow Selected + + + true + + + true + + + + + + + Deny All + + + true + + + + + + + + + + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + +
diff --git a/src/fdosecrets/widgets/RowButtonHelper.cpp b/src/fdosecrets/widgets/RowButtonHelper.cpp new file mode 100644 index 00000000..b2f81680 --- /dev/null +++ b/src/fdosecrets/widgets/RowButtonHelper.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "RowButtonHelper.h" + +#include +#include +#include + +#include + +namespace +{ + class WidgetItemDelegate : public QStyledItemDelegate + { + std::function m_create; + + public: + explicit WidgetItemDelegate(QObject* parent, std::function&& create) + : QStyledItemDelegate(parent) + , m_create(std::move(create)) + { + } + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex& index) const override + { + if (!index.isValid()) + return nullptr; + return m_create(parent, index); + } + }; +} // namespace + +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create) +{ + auto delegate = new WidgetItemDelegate(view, std::move(create)); + // doesn't take ownership + view->setItemDelegateForColumn(column, delegate); + + for (int row = 0; row != view->model()->rowCount({}); ++row) { + view->openPersistentEditor(view->model()->index(row, column)); + } + QObject::connect(view->model(), + &QAbstractItemModel::rowsInserted, + delegate, + [view, column](const QModelIndex&, int first, int last) { + for (int i = first; i <= last; ++i) { + auto idx = view->model()->index(i, column); + view->openPersistentEditor(idx); + } + }); +} diff --git a/src/fdosecrets/widgets/RowButtonHelper.h b/src/fdosecrets/widgets/RowButtonHelper.h new file mode 100644 index 00000000..47678713 --- /dev/null +++ b/src/fdosecrets/widgets/RowButtonHelper.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_ROWBUTTONHELPER_H +#define KEEPASSXC_FDOSECRETS_ROWBUTTONHELPER_H + +#include + +class QAbstractItemView; +class QWidget; +class QModelIndex; + +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create); + +/** + * @brief Open an editor on the cell, the editor's user property will be edited. + */ +template +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create) +{ + installWidgetItemDelegate(view, column, [create](QWidget* p, const QModelIndex& idx) { return create(p, idx); }); +} + +#endif // KEEPASSXC_FDOSECRETS_ROWBUTTONHELPER_H diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index 70372a2a..aa2b33ad 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -19,11 +19,10 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Service.h" #include "fdosecrets/objects/Session.h" -#include "core/Database.h" -#include "core/DatabaseIcons.h" #include "gui/DatabaseTabWidget.h" #include "gui/DatabaseWidget.h" #include "gui/Icons.h" @@ -244,35 +243,22 @@ namespace FdoSecrets } } - SettingsSessionModel::SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent) + SettingsClientModel::SettingsClientModel(DBusMgr& dbus, QObject* parent) : QAbstractTableModel(parent) - , m_service(nullptr) + , m_dbus(dbus) { - setService(plugin->serviceInstance()); - connect(plugin, &FdoSecretsPlugin::secretServiceStarted, this, [plugin, this]() { - setService(plugin->serviceInstance()); - }); - connect(plugin, &FdoSecretsPlugin::secretServiceStopped, this, [this]() { setService(nullptr); }); + populateModel(); } - void SettingsSessionModel::setService(Service* service) - { - auto old = m_service; - m_service = service; - if (old != m_service) { - populateModel(); - } - } - - int SettingsSessionModel::rowCount(const QModelIndex& parent) const + int SettingsClientModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } - return m_sessions.size(); + return m_clients.size(); } - int SettingsSessionModel::columnCount(const QModelIndex& parent) const + int SettingsClientModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; @@ -280,7 +266,7 @@ namespace FdoSecrets return 2; } - QVariant SettingsSessionModel::headerData(int section, Qt::Orientation orientation, int role) const + QVariant SettingsClientModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal) { return {}; @@ -300,89 +286,88 @@ namespace FdoSecrets } } - QVariant SettingsSessionModel::data(const QModelIndex& index, int role) const + QVariant SettingsClientModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return {}; } - const auto& sess = m_sessions[index.row()]; - if (!sess) { + const auto& client = m_clients[index.row()]; + if (!client) { return {}; } switch (index.column()) { case 0: - return dataForApplication(sess, role); + return dataForApplication(client, role); case 1: - return dataForManage(sess, role); + return dataForManage(client, role); default: return {}; } } - QVariant SettingsSessionModel::dataForApplication(Session* sess, int role) const + QVariant SettingsClientModel::dataForApplication(const DBusClientPtr& client, int role) const { switch (role) { case Qt::DisplayRole: - return sess->peer(); + return client->name(); default: return {}; } } - QVariant SettingsSessionModel::dataForManage(Session* sess, int role) const + QVariant SettingsClientModel::dataForManage(const DBusClientPtr& client, int role) const { switch (role) { case Qt::EditRole: { - return QVariant::fromValue(sess); + return QVariant::fromValue(client); } default: return {}; } } - void SettingsSessionModel::populateModel() + void SettingsClientModel::populateModel() { beginResetModel(); - m_sessions.clear(); + m_clients.clear(); - if (m_service) { - // Add existing database tabs - for (const auto& sess : m_service->sessions()) { - sessionAdded(sess, false); - } - - // connect signals - connect(m_service, &Service::sessionOpened, this, [this](Session* sess) { sessionAdded(sess, true); }); - connect(m_service, &Service::sessionClosed, this, &SettingsSessionModel::sessionRemoved); + // Add existing database tabs + for (const auto& client : m_dbus.clients()) { + clientConnected(client, false); } + // connect signals + connect(&m_dbus, &DBusMgr::clientConnected, this, [this](const DBusClientPtr& client) { + clientConnected(client, true); + }); + connect(&m_dbus, &DBusMgr::clientDisconnected, this, &SettingsClientModel::clientDisconnected); + endResetModel(); } - void SettingsSessionModel::sessionAdded(Session* sess, bool emitSignals) + void SettingsClientModel::clientConnected(const DBusClientPtr& client, bool emitSignals) { - int row = m_sessions.size(); + int row = m_clients.size(); if (emitSignals) { beginInsertRows({}, row, row); } - m_sessions.append(sess); + m_clients.append(client); if (emitSignals) { endInsertRows(); } } - void SettingsSessionModel::sessionRemoved(Session* sess) + void SettingsClientModel::clientDisconnected(const DBusClientPtr& client) { - for (int i = 0; i != m_sessions.size(); i++) { - if (m_sessions[i] == sess) { + for (int i = 0; i != m_clients.size(); i++) { + if (m_clients[i] == client) { beginRemoveRows({}, i, i); - m_sessions[i]->disconnect(this); - m_sessions.removeAt(i); + m_clients.removeAt(i); endRemoveRows(); break; diff --git a/src/fdosecrets/widgets/SettingsModels.h b/src/fdosecrets/widgets/SettingsModels.h index b07bb163..e933f5cf 100644 --- a/src/fdosecrets/widgets/SettingsModels.h +++ b/src/fdosecrets/widgets/SettingsModels.h @@ -18,12 +18,13 @@ #ifndef KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H #define KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H +#include "fdosecrets/dbus/DBusClient.h" + #include #include class DatabaseTabWidget; class DatabaseWidget; -class FdoSecretsPlugin; namespace FdoSecrets { @@ -58,14 +59,13 @@ namespace FdoSecrets QList> m_dbs; }; - class Service; - class Session; + class DBusMgr; - class SettingsSessionModel : public QAbstractTableModel + class SettingsClientModel : public QAbstractTableModel { Q_OBJECT public: - explicit SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent = nullptr); + explicit SettingsClientModel(DBusMgr& dbus, QObject* parent = nullptr); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; @@ -73,22 +73,20 @@ namespace FdoSecrets QVariant headerData(int section, Qt::Orientation orientation, int role) const override; private: - void setService(Service* service); - - QVariant dataForApplication(Session* sess, int role) const; - QVariant dataForManage(Session* sess, int role) const; + QVariant dataForApplication(const DBusClientPtr& client, int role) const; + QVariant dataForManage(const DBusClientPtr& client, int role) const; private slots: void populateModel(); - void sessionAdded(Session* sess, bool emitSignals); - void sessionRemoved(Session* sess); + void clientConnected(const DBusClientPtr& client, bool emitSignals); + void clientDisconnected(const DBusClientPtr& client); private: // source - QPointer m_service; + DBusMgr& m_dbus; // internal copy, so we can emit with changed index - QList m_sessions; + QList m_clients; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 73169593..5b7b8054 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -20,24 +20,21 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Session.h" +#include "fdosecrets/widgets/RowButtonHelper.h" #include "fdosecrets/widgets/SettingsModels.h" #include "gui/DatabaseWidget.h" #include "gui/Icons.h" #include -#include -#include #include -#include -#include #include -#include -using FdoSecrets::Session; +using FdoSecrets::DBusClientPtr; +using FdoSecrets::SettingsClientModel; using FdoSecrets::SettingsDatabaseModel; -using FdoSecrets::SettingsSessionModel; namespace { @@ -158,10 +155,10 @@ namespace { Q_OBJECT - Q_PROPERTY(Session* session READ session WRITE setSession USER true) + Q_PROPERTY(const DBusClientPtr& client READ client WRITE setClient USER true) public: - explicit ManageSession(FdoSecretsPlugin*, QWidget* parent = nullptr) + explicit ManageSession(QWidget* parent = nullptr) : QToolBar(parent) { setFloatable(false); @@ -173,15 +170,15 @@ namespace spacer->setVisible(true); addWidget(spacer); - m_disconnectAct = new QAction(tr("Disconnect"), this); - m_disconnectAct->setIcon(icons()->icon(QStringLiteral("dialog-close"))); - m_disconnectAct->setToolTip(tr("Disconnect this application")); - connect(m_disconnectAct, &QAction::triggered, this, [this]() { - if (m_session) { - m_session->close(); + auto disconnectAct = new QAction(tr("Disconnect"), this); + disconnectAct->setIcon(icons()->icon(QStringLiteral("dialog-close"))); + disconnectAct->setToolTip(tr("Disconnect this application")); + connect(disconnectAct, &QAction::triggered, this, [this]() { + if (m_client) { + m_client->disconnectDBus(); } }); - addAction(m_disconnectAct); + addAction(disconnectAct); // use a dummy widget to center the buttons spacer = new QWidget(this); @@ -190,71 +187,46 @@ namespace addWidget(spacer); } - Session* session() + const DBusClientPtr& client() const { - return m_session; + return m_client; } - void setSession(Session* sess) + void setClient(DBusClientPtr client) { - m_session = sess; + m_client = std::move(client); } private: - Session* m_session = nullptr; - QAction* m_disconnectAct = nullptr; - }; - - template class Creator : public QItemEditorCreatorBase - { - public: - inline explicit Creator(FdoSecretsPlugin* plugin) - : QItemEditorCreatorBase() - , m_plugin(plugin) - , m_propertyName(T::staticMetaObject.userProperty().name()) - { - } - - inline QWidget* createWidget(QWidget* parent) const override - { - return new T(m_plugin, parent); - } - - inline QByteArray valuePropertyName() const override - { - return m_propertyName; - } - - private: - FdoSecretsPlugin* m_plugin; - QByteArray m_propertyName; + DBusClientPtr m_client{}; }; } // namespace SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) : QWidget(parent) , m_ui(new Ui::SettingsWidgetFdoSecrets()) - , m_factory(new QItemEditorFactory) , m_plugin(plugin) { m_ui->setupUi(this); m_ui->warningMsg->setHidden(true); m_ui->warningMsg->setCloseButtonVisible(false); - auto sessModel = new SettingsSessionModel(plugin, this); - m_ui->tableSessions->setModel(sessModel); - setupView(m_ui->tableSessions, 1, qMetaTypeId(), new Creator(m_plugin)); + auto clientModel = new SettingsClientModel(*plugin->dbus(), this); + m_ui->tableClients->setModel(clientModel); + installWidgetItemDelegate( + m_ui->tableClients, 1, [](QWidget* p, const QModelIndex&) { return new ManageSession(p); }); // config header after setting model, otherwise the header doesn't have enough sections - auto sessViewHeader = m_ui->tableSessions->horizontalHeader(); - sessViewHeader->setSelectionMode(QAbstractItemView::NoSelection); - sessViewHeader->setSectionsClickable(false); - sessViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application - sessViewHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button + auto clientViewHeader = m_ui->tableClients->horizontalHeader(); + clientViewHeader->setSelectionMode(QAbstractItemView::NoSelection); + clientViewHeader->setSectionsClickable(false); + clientViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application + clientViewHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button auto dbModel = new SettingsDatabaseModel(plugin->dbTabs(), this); m_ui->tableDatabases->setModel(dbModel); - setupView(m_ui->tableDatabases, 2, qMetaTypeId(), new Creator(m_plugin)); + installWidgetItemDelegate( + m_ui->tableDatabases, 2, [plugin](QWidget* p, const QModelIndex&) { return new ManageDatabase(plugin, p); }); // config header after setting model, otherwise the header doesn't have enough sections auto dbViewHeader = m_ui->tableDatabases->horizontalHeader(); @@ -277,40 +249,22 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi connect(m_plugin, SIGNAL(secretServiceStopped()), &m_checkTimer, SLOT(start())); } -void SettingsWidgetFdoSecrets::setupView(QAbstractItemView* view, - int manageColumn, - int editorTypeId, - QItemEditorCreatorBase* creator) -{ - auto manageButtonDelegate = new QStyledItemDelegate(this); - m_factory->registerEditor(editorTypeId, creator); - manageButtonDelegate->setItemEditorFactory(m_factory.data()); - view->setItemDelegateForColumn(manageColumn, manageButtonDelegate); - connect(view->model(), - &QAbstractItemModel::rowsInserted, - this, - [view, manageColumn](const QModelIndex&, int first, int last) { - for (int i = first; i <= last; ++i) { - auto idx = view->model()->index(i, manageColumn); - view->openPersistentEditor(idx); - } - }); -} - SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default; void SettingsWidgetFdoSecrets::loadSettings() { m_ui->enableFdoSecretService->setChecked(FdoSecrets::settings()->isEnabled()); m_ui->showNotification->setChecked(FdoSecrets::settings()->showNotification()); - m_ui->noConfirmDeleteItem->setChecked(FdoSecrets::settings()->noConfirmDeleteItem()); + m_ui->confirmDeleteItem->setChecked(FdoSecrets::settings()->confirmDeleteItem()); + m_ui->confirmAccessItem->setChecked(FdoSecrets::settings()->confirmAccessItem()); } void SettingsWidgetFdoSecrets::saveSettings() { FdoSecrets::settings()->setEnabled(m_ui->enableFdoSecretService->isChecked()); FdoSecrets::settings()->setShowNotification(m_ui->showNotification->isChecked()); - FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); + FdoSecrets::settings()->setConfirmDeleteItem(m_ui->confirmDeleteItem->isChecked()); + FdoSecrets::settings()->setConfirmAccessItem(m_ui->confirmAccessItem->isChecked()); } void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) @@ -333,17 +287,9 @@ void SettingsWidgetFdoSecrets::checkDBusName() return; } - auto reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral(DBUS_SERVICE_SECRET)); - if (!reply.isValid()) { + if (m_plugin->dbus()->serviceOccupied()) { m_ui->warningMsg->showMessage( - tr("Error: Failed to connect to DBus. Please check your DBus setup."), MessageWidget::Error, -1); - m_ui->enableFdoSecretService->setChecked(false); - m_ui->enableFdoSecretService->setEnabled(false); - return; - } - if (reply.value()) { - m_ui->warningMsg->showMessage( - tr("Warning: ") + m_plugin->reportExistingService(), MessageWidget::Warning, -1); + tr("Warning: ") + m_plugin->dbus()->reportExistingService(), MessageWidget::Warning, -1); m_ui->enableFdoSecretService->setChecked(false); m_ui->enableFdoSecretService->setEnabled(false); return; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index c323b390..c4a58a5e 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -25,16 +25,6 @@ #include class QAbstractItemView; -class QItemEditorCreatorBase; -class QItemEditorFactory; - -namespace FdoSecrets -{ - - class Session; - class Collection; - -} // namespace FdoSecrets class FdoSecretsPlugin; @@ -61,12 +51,8 @@ protected: void showEvent(QShowEvent* event) override; void hideEvent(QHideEvent* event) override; -private: - void setupView(QAbstractItemView* view, int manageColumn, int editorTypeId, QItemEditorCreatorBase* creator); - private: QScopedPointer m_ui; - QScopedPointer m_factory; FdoSecretsPlugin* m_plugin; QTimer m_checkTimer; }; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index abc15d56..7034d7bd 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -49,17 +49,36 @@ - Show notification when credentials are requested + Show notification when passwords are retrieved by clients + + + true - + - <html><head/><body><p>If recycle bin is enabled for the database, entries will be moved to recycle bin directly. Otherwise, they will be deleted without confirmation.</p><p>You will still be prompted if any entries are referenced by others.</p></body></html> + <html><head/><body><p>If enabled, any attempt to read a password must be confirmed. Otherwise, clients can read passwords without confirmation when the database is unlocked.</p><p>This option only covers the access to the password of an entry. Clients can always enumerate the items of exposed databases and query their attributes.</p></body></html> - Don't confirm when entries are deleted by clients + Confirm when passwords are retrieved by clients + + + true + + + + + + + <html><head/><body><p><span style=" font-family:'-apple-system','BlinkMacSystemFont','Segoe UI','Helvetica','Arial','sans-serif','Apple Color Emoji','Segoe UI Emoji'; font-size:14px; color:#24292e; background-color:#ffffff;">This setting does not override disabling recycle bin prompts</span></p></body></html> + + + Confirm when clients request entry deletion + + + true @@ -120,7 +139,7 @@
- + Qt::NoFocus diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 2d640aeb..41c1cc6a 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -474,20 +474,20 @@ void DatabaseWidget::deleteSelectedEntries() deleteEntries(std::move(selectedEntries)); } -void DatabaseWidget::deleteEntries(QList selectedEntries) +void DatabaseWidget::deleteEntries(QList selectedEntries, bool confirm) { // Confirm entry removal before moving forward auto* recycleBin = m_db->metadata()->recycleBin(); bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid())) || !m_db->metadata()->recycleBinEnabled(); - if (!confirmDeleteEntries(selectedEntries, permanent)) { + if (confirm && !confirmDeleteEntries(selectedEntries, permanent)) { return; } // Find references to selected entries and prompt for direction if necessary auto it = selectedEntries.begin(); - while (it != selectedEntries.end()) { + while (confirm && it != selectedEntries.end()) { auto references = m_db->rootGroup()->referencesRecursive(*it); if (!references.isEmpty()) { // Ignore references that are selected for deletion diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index efe60212..fc8d9ee7 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -162,7 +162,7 @@ public slots: void createEntry(); void cloneEntry(); void deleteSelectedEntries(); - void deleteEntries(QList entries); + void deleteEntries(QList entries, bool confirm = true); void focusOnEntries(bool editIfFocused = false); void focusOnGroups(bool editIfFocused = false); void moveEntryUp(); diff --git a/tests/TestFdoSecrets.cpp b/tests/TestFdoSecrets.cpp index 299c3f7b..eba97f67 100644 --- a/tests/TestFdoSecrets.cpp +++ b/tests/TestFdoSecrets.cpp @@ -20,13 +20,13 @@ #include "TestGlobal.h" #include "core/EntrySearcher.h" +#include "crypto/Crypto.h" #include "fdosecrets/GcryptMPI.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/SessionCipher.h" -#include "crypto/Crypto.h" - QTEST_GUILESS_MAIN(TestFdoSecrets) void TestFdoSecrets::initTestCase() @@ -144,3 +144,39 @@ void TestFdoSecrets::testSpecialCharsInAttributeValue() QCOMPARE(res[0]->title(), QStringLiteral("titleB")); } } + +void TestFdoSecrets::testDBusPathParse() +{ + using FdoSecrets::DBusMgr; + using PathType = FdoSecrets::DBusMgr::PathType; + + auto parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets")); + QCOMPARE(parsed.type, PathType::Service); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/collection/xxx")); + QCOMPARE(parsed.type, PathType::Collection); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/collection/xxx/yyy")); + QCOMPARE(parsed.type, PathType::Item); + QCOMPARE(parsed.id, QStringLiteral("yyy")); + QCOMPARE(parsed.parentId, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/aliases/xxx")); + QCOMPARE(parsed.type, PathType::Aliases); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/session/xxx")); + QCOMPARE(parsed.type, PathType::Session); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/prompt/xxx")); + QCOMPARE(parsed.type, PathType::Prompt); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/other/prompt/xxx")); + QCOMPARE(parsed.type, PathType::Unknown); + + parsed = DBusMgr::parsePath(QStringLiteral("/org")); + QCOMPARE(parsed.type, PathType::Unknown); +} diff --git a/tests/TestFdoSecrets.h b/tests/TestFdoSecrets.h index 1cecbbea..c41a6578 100644 --- a/tests/TestFdoSecrets.h +++ b/tests/TestFdoSecrets.h @@ -32,6 +32,7 @@ private slots: void testDhIetf1024Sha256Aes128CbcPkcs7(); void testCrazyAttributeKey(); void testSpecialCharsInAttributeValue(); + void testDBusPathParse(); }; #endif // KEEPASSXC_TESTFDOSECRETS_H diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index a6d6adb1..dfffcc1e 100644 Binary files a/tests/data/NewDatabase.kdbx and b/tests/data/NewDatabase.kdbx differ diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml deleted file mode 100644 index 3b5dd64f..00000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml deleted file mode 100644 index d9c39a2e..00000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml deleted file mode 100644 index 92aa8df8..00000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml deleted file mode 100644 index 40240bb4..00000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml deleted file mode 100644 index 7d358df7..00000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt index 1d5822d2..3264da51 100644 --- a/tests/gui/CMakeLists.txt +++ b/tests/gui/CMakeLists.txt @@ -24,7 +24,7 @@ endif() if(WITH_XC_FDOSECRETS) add_unit_test(NAME testguifdosecrets - SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp + SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp ../util/FdoSecretsProxy.cpp LIBS ${TEST_LIBRARIES} # The following doesn't work because dbus-run-session expects execname to be in PATH # dbus-run-session -- execname diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp index eb8192d5..55502127 100644 --- a/tests/gui/TestGuiFdoSecrets.cpp +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -19,12 +19,12 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" -#include "fdosecrets/objects/Prompt.h" -#include "fdosecrets/objects/Service.h" -#include "fdosecrets/objects/Session.h" #include "fdosecrets/objects/SessionCipher.h" +#include "fdosecrets/widgets/AccessControlDialog.h" #include "TestGlobal.h" #include "config-keepassx-tests.h" @@ -39,17 +39,13 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/wizard/NewDatabaseWizard.h" +#include "util/FdoSecretsProxy.h" #include "util/TemporaryFile.h" -#include -#include -#include -#include +#include #include -#include #include #include -#include #include #include @@ -74,111 +70,77 @@ int main(int argc, char* argv[]) #define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default" -#define VERIFY(statement) \ +// assert macros compatible with function having return values +#define VERIFY2_RET(statement, msg) \ do { \ - if (!QTest::qVerify(static_cast(statement), #statement, "", __FILE__, __LINE__)) \ + if (!QTest::qVerify(static_cast(statement), #statement, (msg), __FILE__, __LINE__)) \ return {}; \ } while (false) -#define COMPARE(actual, expected) \ +#define COMPARE_RET(actual, expected) \ do { \ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ return {}; \ } while (false) -#define FAIL(message) \ +// by default use these with Qt macros +#define VERIFY QVERIFY +#define COMPARE QCOMPARE +#define VERIFY2 QVERIFY2 + +#define DBUS_COMPARE(actual, expected) \ do { \ - QTest::qFail(static_cast(message), __FILE__, __LINE__); \ - return {}; \ + auto reply = (actual); \ + VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \ + COMPARE(reply.value(), (expected)); \ } while (false) -#define COMPARE_DBUS_LOCAL_CALL(actual, expected) \ +#define DBUS_VERIFY(stmt) \ do { \ - const auto a = (actual); \ - QVERIFY(!a.isError()); \ - QCOMPARE(a.value(), (expected)); \ + auto reply = (stmt); \ + VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \ } while (false) -#define CHECKED_DBUS_LOCAL_CALL(name, stmt) \ - std::remove_cv::type name; \ +#define DBUS_GET(var, stmt) \ + std::remove_cv())>::type var; \ do { \ - const auto rep = stmt; \ - QVERIFY(!rep.isError()); \ - name = rep.value(); \ + const auto rep = (stmt); \ + VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \ + var = rep.argumentAt<0>(); \ } while (false) -namespace -{ - std::unique_ptr interfaceOf(const QDBusObjectPath& objPath, const QString& interface) - { - std::unique_ptr iface(new QDBusInterface(DBUS_SERVICE_SECRET, objPath.path(), interface)); - iface->setTimeout(5); - VERIFY(iface->isValid()); - return iface; - } - - std::unique_ptr interfaceOf(FdoSecrets::DBusObject* obj) - { - VERIFY(obj); - auto metaAdaptor = obj->dbusAdaptor().metaObject(); - auto ifaceName = metaAdaptor->classInfo(metaAdaptor->indexOfClassInfo("D-Bus Interface")).value(); - - return interfaceOf(obj->objectPath(), ifaceName); - } - - template QString extractElement(const QString& doc, T cond) - { - QXmlStreamReader reader(doc); - while (!reader.atEnd()) { - int st = reader.characterOffset(); - - if (reader.readNext() != QXmlStreamReader::StartElement || !cond(reader)) { - continue; - } - - reader.skipCurrentElement(); - if (reader.hasError()) { - break; - } - - // remove whitespaces between elements to be a little bit flexible - int ed = reader.characterOffset(); - return doc.mid(st - 1, ed - st + 1).replace(QRegularExpression(R"(>[\s\n]+<)"), "><"); - } - VERIFY(!reader.hasError()); - return {}; - } - - bool checkDBusSpec(const QString& path, const QString& interface) - { - QFile f(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/dbus/interfaces/%1.xml").arg(interface)); - VERIFY(f.open(QFile::ReadOnly | QFile::Text)); - QTextStream in(&f); - auto spec = in.readAll().replace(QRegularExpression(R"(>[\s\n]+<)"), "><").trimmed(); - - auto bus = QDBusConnection::sessionBus(); - auto msg = QDBusMessage::createMethodCall( - DBUS_SERVICE_SECRET, path, "org.freedesktop.DBus.Introspectable", "Introspect"); - - // BlockWithGui enters event loop - auto reply = QDBusPendingReply(bus.call(msg, QDBus::BlockWithGui, 5)); - VERIFY(reply.isValid()); - auto actual = extractElement(reply.argumentAt<0>(), [&](const QXmlStreamReader& reader) { - return reader.name() == "interface" && reader.attributes().value("name") == interface; - }); - - COMPARE(actual, spec); - return true; - } -} // namespace +#define DBUS_GET2(name1, name2, stmt) \ + std::remove_cv())>::type name1; \ + std::remove_cv())>::type name2; \ + do { \ + const auto rep = (stmt); \ + VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \ + name1 = rep.argumentAt<0>(); \ + name2 = rep.argumentAt<1>(); \ + } while (false) using namespace FdoSecrets; +class FakeClient : public DBusClient +{ +public: + explicit FakeClient(DBusMgr* dbus) + : DBusClient(dbus, QStringLiteral("local"), 0, "fake-client") + { + } +}; + +// pretty print QDBusObjectPath in QCOMPARE +char* toString(const QDBusObjectPath& path) +{ + return QTest::toString("ObjectPath(" + path.path() + ")"); +} + TestGuiFdoSecrets::~TestGuiFdoSecrets() = default; void TestGuiFdoSecrets::initTestCase() { - QVERIFY(Crypto::init()); + VERIFY(Crypto::init()); Config::createTempFileInstance(); config()->set(Config::AutoSaveAfterEveryChange, false); config()->set(Config::AutoSaveOnExit, false); @@ -193,15 +155,15 @@ void TestGuiFdoSecrets::initTestCase() m_mainWindow.reset(new MainWindow()); m_tabWidget = m_mainWindow->findChild("tabWidget"); - QVERIFY(m_tabWidget); + VERIFY(m_tabWidget); m_plugin = FdoSecretsPlugin::getPlugin(); - QVERIFY(m_plugin); + VERIFY(m_plugin); m_mainWindow->show(); // Load the NewDatabase.kdbx file into temporary storage QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx")); - QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); - QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); + VERIFY(sourceDbFile.open(QIODevice::ReadOnly)); + VERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); sourceDbFile.close(); // set keys for session encryption @@ -223,23 +185,27 @@ void TestGuiFdoSecrets::initTestCase() "ab5c26b0ea3480c9aba8154cf"); // use the same cipher to do the client side encryption, but exchange the position of client/server keys m_cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7); - QVERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)), - MpiFromHex("30d18c6b328bac970c05bda6af2e708b9" - "d6bbbb6dc136c1a2d96e870fabc86ad74" - "1846a26a4197f32f65ea2e7580ad2afe3" - "dd5d6c1224b8368b0df2cd75d520a9ff9" - "7fe894cc7da71b7bd285b4633359c16c8" - "d341f822fa4f0fdf59b5d3448658c46a2" - "a86dbb14ff85823873f8a259ccc52bbb8" - "2b5a4c2a75447982553b42221"), - MpiFromHex("84aafe9c9f356f7762307f4d791acb59e" - "8e3fd562abdbb481d0587f8400ad6c51d" - "af561a1beb9a22c8cd4d2807367c5787b" - "2e06d631ccbb5194b6bb32211583ce688" - "f9c2cebc22a9e4d494d12ebdd570c61a1" - "62a94e88561d25ccd0415339d1f59e1b0" - "6bc6b6b5fde46e23b2410eb034be390d3" - "2407ec7ae90f0831f24afd5ac"))); + VERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)), + MpiFromHex("30d18c6b328bac970c05bda6af2e708b9" + "d6bbbb6dc136c1a2d96e870fabc86ad74" + "1846a26a4197f32f65ea2e7580ad2afe3" + "dd5d6c1224b8368b0df2cd75d520a9ff9" + "7fe894cc7da71b7bd285b4633359c16c8" + "d341f822fa4f0fdf59b5d3448658c46a2" + "a86dbb14ff85823873f8a259ccc52bbb8" + "2b5a4c2a75447982553b42221"), + MpiFromHex("84aafe9c9f356f7762307f4d791acb59e" + "8e3fd562abdbb481d0587f8400ad6c51d" + "af561a1beb9a22c8cd4d2807367c5787b" + "2e06d631ccbb5194b6bb32211583ce688" + "f9c2cebc22a9e4d494d12ebdd570c61a1" + "62a94e88561d25ccd0415339d1f59e1b0" + "6bc6b6b5fde46e23b2410eb034be390d3" + "2407ec7ae90f0831f24afd5ac"))); + + // set a fake dbus client all the time so we can freely access DBusMgr anywhere + m_client.reset(new FakeClient(m_plugin->dbus().data())); + m_plugin->dbus()->overrideClient(m_client); } // Every test starts with opening the temp database @@ -247,8 +213,8 @@ void TestGuiFdoSecrets::init() { m_dbFile.reset(new TemporaryFile()); // Write the temp storage to a temp database file for use in our tests - QVERIFY(m_dbFile->open()); - QCOMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); + VERIFY(m_dbFile->open()); + COMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); m_dbFile->close(); // make sure window is activated or focus tests may fail @@ -262,7 +228,7 @@ void TestGuiFdoSecrets::init() // by default expose the root group FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); - QVERIFY(m_dbWidget->save()); + VERIFY(m_dbWidget->save()); } // Every test ends with closing the temp database without saving @@ -270,6 +236,7 @@ void TestGuiFdoSecrets::cleanup() { // restore to default settings FdoSecrets::settings()->setShowNotification(false); + FdoSecrets::settings()->setConfirmAccessItem(false); FdoSecrets::settings()->setEnabled(false); if (m_plugin) { m_plugin->updateServiceState(); @@ -279,93 +246,57 @@ void TestGuiFdoSecrets::cleanup() for (int i = 0; i != m_tabWidget->count(); ++i) { m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean(); } - QVERIFY(m_tabWidget->closeAllDatabaseTabs()); + VERIFY(m_tabWidget->closeAllDatabaseTabs()); QApplication::processEvents(); if (m_dbFile) { m_dbFile->remove(); } + + m_client->clearAuthorization(); } void TestGuiFdoSecrets::cleanupTestCase() { + m_plugin->dbus()->overrideClient({}); if (m_dbFile) { m_dbFile->remove(); } } -void TestGuiFdoSecrets::testDBusSpec() -{ - auto service = enableService(); - QVERIFY(service); - - // service - QCOMPARE(service->objectPath().path(), QStringLiteral(DBUS_PATH_SECRETS)); - QVERIFY(checkDBusSpec(service->objectPath().path(), DBUS_INTERFACE_SECRET_SERVICE)); - - // default alias - QVERIFY(checkDBusSpec(DBUS_PATH_DEFAULT_ALIAS, DBUS_INTERFACE_SECRET_COLLECTION)); - - // collection - auto coll = getDefaultCollection(service); - QVERIFY(coll); - QVERIFY(checkDBusSpec(coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION)); - - // item - auto item = getFirstItem(coll); - QVERIFY(item); - QVERIFY(checkDBusSpec(item->objectPath().path(), DBUS_INTERFACE_SECRET_ITEM)); - - // session - auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); - QVERIFY(checkDBusSpec(sess->objectPath().path(), DBUS_INTERFACE_SECRET_SESSION)); - - // prompt - FdoSecrets::settings()->setNoConfirmDeleteItem(true); - PromptBase* prompt = nullptr; - { - auto rep = item->deleteItem(); - QVERIFY(!rep.isError()); - prompt = rep.value(); - } - QVERIFY(prompt); - QVERIFY(checkDBusSpec(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT)); -} - void TestGuiFdoSecrets::testServiceEnable() { QSignalSpy sigError(m_plugin, SIGNAL(error(QString))); - QVERIFY(sigError.isValid()); + VERIFY(sigError.isValid()); QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted())); - QVERIFY(sigStarted.isValid()); + VERIFY(sigStarted.isValid()); // make sure no one else is holding the service - QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); // enable the service auto service = enableService(); - QVERIFY(service); + VERIFY(service); // service started without error - QVERIFY(sigError.isEmpty()); - QCOMPARE(sigStarted.size(), 1); + VERIFY(sigError.isEmpty()); + COMPARE(sigStarted.size(), 1); QApplication::processEvents(); - QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); // there will be one default collection auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); - COMPARE_DBUS_LOCAL_CALL(coll->label(), m_db->metadata()->name()); - COMPARE_DBUS_LOCAL_CALL( - coll->created(), - static_cast(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000)); - COMPARE_DBUS_LOCAL_CALL( + DBUS_COMPARE(coll->locked(), false); + DBUS_COMPARE(coll->label(), m_db->metadata()->name()); + + DBUS_COMPARE(coll->created(), + static_cast(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000)); + DBUS_COMPARE( coll->modified(), static_cast(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000)); } @@ -375,69 +306,66 @@ void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase() // reset the exposed group and then enable the service FdoSecrets::settings()->setExposedGroup(m_db, {}); auto service = enableService(); - QVERIFY(service); + VERIFY(service); // no collections - COMPARE_DBUS_LOCAL_CALL(service->collections(), QList{}); + DBUS_COMPARE(service->collections(), QList{}); } void TestGuiFdoSecrets::testServiceSearch() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); - item->backend()->attributes()->set("fdosecrets-test", "1"); - item->backend()->attributes()->set("fdosecrets-test-protected", "2", true); + auto entries = m_db->rootGroup()->entriesRecursive(false); + VERIFY(!entries.isEmpty()); + const auto& entry = entries.first(); + entry->attributes()->set("fdosecrets-test", "1"); + entry->attributes()->set("fdosecrets-test-protected", "2", true); const QString crazyKey = "_a:bc&-+'-e%12df_d"; const QString crazyValue = "[v]al@-ue"; - item->backend()->attributes()->set(crazyKey, crazyValue); + entry->attributes()->set(crazyKey, crazyValue); // search by title { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Title", item->backend()->title()}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } // search by attribute { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test", "1"}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test", "1"}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{crazyKey, crazyValue}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{crazyKey, crazyValue}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } // searching using empty terms returns nothing { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } // searching using protected attributes or password returns nothing { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Password", item->backend()->password()}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({{"Password", entry->password()}})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test-protected", "2"}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test-protected", "2"}})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } } @@ -446,47 +374,44 @@ void TestGuiFdoSecrets::testServiceUnlock() lockDatabaseInBackend(); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); - QSignalSpy spyCollectionChanged(&service->dbusAdaptor(), SIGNAL(CollectionChanged(QDBusObjectPath))); - QVERIFY(spyCollectionChanged.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); - PromptBase* prompt = nullptr; - { - CHECKED_DBUS_LOCAL_CALL(unlocked, service->unlock({coll.data()}, prompt)); - // nothing is unlocked immediately without user's action - QVERIFY(unlocked.isEmpty()); - } - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // nothing is unlocked yet - QCOMPARE(spyPromptCompleted.count(), 0); - QVERIFY(coll); - QVERIFY(coll->backend()->isLocked()); + QTRY_COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(coll->locked(), true); // drive the prompt - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); // still not unlocked before user action - QCOMPARE(spyPromptCompleted.count(), 0); - QVERIFY(coll); - QVERIFY(coll->backend()->isLocked()); + QTRY_COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(coll->locked(), true); // interact with the dialog QApplication::processEvents(); { auto dbOpenDlg = m_tabWidget->findChild(); - QVERIFY(dbOpenDlg); + VERIFY(dbOpenDlg); auto editPassword = dbOpenDlg->findChild("editPassword"); - QVERIFY(editPassword); + VERIFY(editPassword); editPassword->setFocus(); QTest::keyClicks(editPassword, "a"); QTest::keyClick(editPassword, Qt::Key_Enter); @@ -494,203 +419,286 @@ void TestGuiFdoSecrets::testServiceUnlock() QApplication::processEvents(); // unlocked - QVERIFY(coll); - QVERIFY(!coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), false); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); { auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.size(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().value>(), {coll->objectPath()}); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); } - QCOMPARE(spyCollectionCreated.count(), 0); - QCOMPARE(spyCollectionChanged.count(), 1); + QTRY_COMPARE(spyCollectionCreated.count(), 0); + QTRY_VERIFY(!spyCollectionChanged.isEmpty()); + for (const auto& args : spyCollectionChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); + } + QTRY_COMPARE(spyCollectionDeleted.count(), 0); +} + +void TestGuiFdoSecrets::testServiceUnlockItems() +{ + FdoSecrets::settings()->setConfirmAccessItem(true); + + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + + DBUS_COMPARE(item->locked(), true); + { - auto args = spyCollectionChanged.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // nothing is unlocked yet + COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(item->locked(), true); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + // only allow once + VERIFY(driveAccessControlDialog(false)); + + // unlocked + DBUS_COMPARE(item->locked(), false); + + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); + } } - QCOMPARE(spyCollectionDeleted.count(), 0); + + // access the secret should reset the locking state + { + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + } + DBUS_COMPARE(item->locked(), true); + + // unlock again with remember + { + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // nothing is unlocked yet + COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(item->locked(), true); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + // only allow and remember + VERIFY(driveAccessControlDialog(true)); + + // unlocked + DBUS_COMPARE(item->locked(), false); + + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); + } + } + + // access the secret does not reset the locking state + { + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + } + DBUS_COMPARE(item->locked(), false); } void TestGuiFdoSecrets::testServiceLock() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); - QSignalSpy spyCollectionChanged(&service->dbusAdaptor(), SIGNAL(CollectionChanged(QDBusObjectPath))); - QVERIFY(spyCollectionChanged.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); // if the db is modified, prompt user m_db->markAsModified(); { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click cancel MessageBox::setNextAnswer(MessageBox::Cancel); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(!coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), false); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), true); - QCOMPARE(args.at(1).value>(), {}); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), true); + COMPARE(getSignalVariantArgument>(args.at(1)), {}); } { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), true); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().value>(), {coll->objectPath()}); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); } - QCOMPARE(spyCollectionCreated.count(), 0); - QCOMPARE(spyCollectionChanged.count(), 1); - { - auto args = spyCollectionChanged.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + QTRY_COMPARE(spyCollectionCreated.count(), 0); + QTRY_VERIFY(!spyCollectionChanged.isEmpty()); + for (const auto& args : spyCollectionChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } - QCOMPARE(spyCollectionDeleted.count(), 0); + QTRY_COMPARE(spyCollectionDeleted.count(), 0); // locking item locks the whole db unlockDatabaseInBackend(); { auto item = getFirstItem(coll); - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({item}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(item->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), true); } } void TestGuiFdoSecrets::testSessionOpen() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); - QCOMPARE(service->sessions().size(), 1); + VERIFY(sess); sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); - QCOMPARE(service->sessions().size(), 2); + VERIFY(sess); } void TestGuiFdoSecrets::testSessionClose() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); + VERIFY(sess); - QCOMPARE(service->sessions().size(), 1); - - auto rep = sess->close(); - QVERIFY(!rep.isError()); - - QCOMPARE(service->sessions().size(), 0); + DBUS_VERIFY(sess->Close()); } void TestGuiFdoSecrets::testCollectionCreate() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); // returns existing if alias is nonempty and exists { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL( - coll, service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "NewDB"}}, "default", prompt)); - QVERIFY(!prompt); - QCOMPARE(coll, getDefaultCollection(service).data()); + auto existing = getDefaultCollection(service); + DBUS_GET2(collPath, + promptPath, + service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "NewDB"}}, "default")); + COMPARE(promptPath, QDBusObjectPath("/")); + COMPARE(collPath.path(), existing->path()); } - QCOMPARE(spyCollectionCreated.count(), 0); + QTRY_COMPARE(spyCollectionCreated.count(), 0); // create new one and set properties { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL( - created, - service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "Test NewDB"}}, "mydatadb", prompt)); - QVERIFY(!created); - QVERIFY(prompt); + DBUS_GET2(collPath, + promptPath, + service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb")); + COMPARE(collPath, QDBusObjectPath("/")); + auto prompt = getProxy(promptPath); + VERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); - QTimer::singleShot(50, this, SLOT(createDatabaseCallback())); - QVERIFY(!prompt->prompt("").isError()); + QTimer::singleShot(50, this, &TestGuiFdoSecrets::driveNewDatabaseWizard); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.size(), 2); - QCOMPARE(args.at(0).toBool(), false); - auto coll = - FdoSecrets::pathToObject(args.at(1).value().variant().value()); - QVERIFY(coll); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + auto coll = getProxy(getSignalVariantArgument(args.at(1))); + VERIFY(coll); - QCOMPARE(coll->backend()->database()->metadata()->name(), QStringLiteral("Test NewDB")); + DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB")); - QCOMPARE(spyCollectionCreated.count(), 1); + QTRY_COMPARE(spyCollectionCreated.count(), 1); { args = spyCollectionCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } } } -void TestGuiFdoSecrets::createDatabaseCallback() +void TestGuiFdoSecrets::driveNewDatabaseWizard() { auto wizard = m_tabWidget->findChild(); - QVERIFY(wizard); + VERIFY(wizard); - QCOMPARE(wizard->currentId(), 0); + COMPARE(wizard->currentId(), 0); wizard->next(); wizard->next(); - QCOMPARE(wizard->currentId(), 2); + COMPARE(wizard->currentId(), 2); // enter password auto* passwordEdit = wizard->findChild("enterPasswordEdit"); @@ -701,7 +709,7 @@ void TestGuiFdoSecrets::createDatabaseCallback() // save database to temporary file TemporaryFile tmpFile; - QVERIFY(tmpFile.open()); + VERIFY(tmpFile.open()); tmpFile.close(); fileDialog()->setNextFileName(tmpFile.fileName()); @@ -713,43 +721,69 @@ void TestGuiFdoSecrets::createDatabaseCallback() void TestGuiFdoSecrets::testCollectionDelete() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // save the path which will be gone after the deletion. - auto collPath = coll->objectPath(); + auto collPath = coll->path(); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); m_db->markAsModified(); - CHECKED_DBUS_LOCAL_CALL(prompt, coll->deleteCollection()); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET(promptPath, coll->Delete()); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); - - // closing the tab should have deleted coll if not in testing + // closing the tab should have deleted the database if not in testing // but deleteLater is not processed in QApplication::processEvent // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents - // QVERIFY(!coll); + QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + // however, the object should already be taken down from dbus + { + auto reply = coll->locked(); + VERIFY(reply.isFinished() && reply.isError()); + COMPARE(reply.error().type(), QDBusError::UnknownObject); + } + + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); - QCOMPARE(spyCollectionDeleted.count(), 1); + QTRY_COMPARE(spyCollectionDeleted.count(), 1); { args = spyCollectionDeleted.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), collPath); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), collPath); + } +} + +void TestGuiFdoSecrets::testCollectionChange() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); + + DBUS_VERIFY(coll->setLabel("anotherLabel")); + COMPARE(m_db->metadata()->name(), QStringLiteral("anotherLabel")); + QTRY_COMPARE(spyCollectionChanged.size(), 1); + { + auto args = spyCollectionChanged.takeFirst(); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } } @@ -757,57 +791,58 @@ void TestGuiFdoSecrets::testHiddenFilename() { // when file name contains leading dot, all parts excepting the last should be used // for collection name, and the registration should success - QVERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx")); + VERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx")); // reset is necessary to not hold database longer and cause connections // not cleaned up when the database tab is closed. m_db.reset(); - QVERIFY(m_tabWidget->closeAllDatabaseTabs()); + VERIFY(m_tabWidget->closeAllDatabaseTabs()); m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a"); m_dbWidget = m_tabWidget->currentDatabaseWidget(); m_db = m_dbWidget->database(); // enable the service auto service = enableService(); - QVERIFY(service); + VERIFY(service); // collection is properly registered auto coll = getDefaultCollection(service); - QVERIFY(coll->objectPath().path() != "/"); - QCOMPARE(coll->name(), QStringLiteral(".Name")); + auto collObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(coll->path())); + VERIFY(collObj); + COMPARE(collObj->name(), QStringLiteral(".Name")); } void TestGuiFdoSecrets::testDuplicateName() { QTemporaryDir dir; - QVERIFY(dir.isValid()); + VERIFY(dir.isValid()); // create another file under different path but with the same filename QString anotherFile = dir.path() + "/" + QFileInfo(*m_dbFile).fileName(); m_dbFile->copy(anotherFile); m_tabWidget->addDatabaseTab(anotherFile, false, "a"); auto service = enableService(); - QVERIFY(service); + VERIFY(service); // when two databases have the same name, one of it will have part of its uuid suffixed - const auto pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC"); - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 2); - QCOMPARE(colls[0]->objectPath().path(), pathNoSuffix); - QVERIFY(colls[1]->objectPath().path() != pathNoSuffix); + const QString pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC"); + DBUS_GET(colls, service->collections()); + COMPARE(colls.size(), 2); + COMPARE(colls[0].path(), pathNoSuffix); + VERIFY(colls[1].path() != pathNoSuffix); } void TestGuiFdoSecrets::testItemCreate() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); - QSignalSpy spyItemCreated(&coll->dbusAdaptor(), SIGNAL(ItemCreated(QDBusObjectPath))); - QVERIFY(spyItemCreated.isValid()); + QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath))); + VERIFY(spyItemCreated.isValid()); // create item StringStringMap attributes{ @@ -816,56 +851,90 @@ void TestGuiFdoSecrets::testItemCreate() }; auto item = createItem(sess, coll, "abc", "Password", attributes, false); - QVERIFY(item); + VERIFY(item); // signals { - QCOMPARE(spyItemCreated.count(), 1); + QTRY_COMPARE(spyItemCreated.count(), 1); auto args = spyItemCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); } // attributes { - CHECKED_DBUS_LOCAL_CALL(actual, item->attributes()); + DBUS_GET(actual, item->attributes()); for (const auto& key : attributes.keys()) { - QVERIFY(actual.contains(key)); - QCOMPARE(actual[key], attributes[key]); + COMPARE(actual[key], attributes[key]); } } // label - COMPARE_DBUS_LOCAL_CALL(item->label(), QStringLiteral("abc")); + DBUS_COMPARE(item->label(), QStringLiteral("abc")); // secrets { - CHECKED_DBUS_LOCAL_CALL(ss, item->getSecret(sess)); - ss = m_cipher->decrypt(ss); - QCOMPARE(ss.value, QByteArray("Password")); + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + auto decrypted = m_cipher->decrypt(ss.unmarshal(m_plugin->dbus())); + COMPARE(decrypted.value, QByteArrayLiteral("Password")); } // searchable { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems(attributes, locked)); - QCOMPARE(locked, QList{}); - QCOMPARE(unlocked, QList{item}); + DBUS_GET2(unlocked, locked, service->SearchItems(attributes)); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } { - CHECKED_DBUS_LOCAL_CALL(unlocked, coll->searchItems(attributes)); - QVERIFY(unlocked.contains(item)); + DBUS_GET(unlocked, coll->SearchItems(attributes)); + VERIFY(unlocked.contains(QDBusObjectPath(item->path()))); + } +} + +void TestGuiFdoSecrets::testItemChange() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + + QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath))); + VERIFY(spyItemChanged.isValid()); + + DBUS_VERIFY(item->setLabel("anotherLabel")); + COMPARE(entry->title(), QStringLiteral("anotherLabel")); + QTRY_VERIFY(!spyItemChanged.isEmpty()); + for (const auto& args : spyItemChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); + } + + spyItemChanged.clear(); + DBUS_VERIFY(item->setAttributes({ + {"abc", "def"}, + })); + COMPARE(entry->attributes()->value("abc"), QStringLiteral("def")); + QTRY_VERIFY(!spyItemChanged.isEmpty()); + for (const auto& args : spyItemChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); } } void TestGuiFdoSecrets::testItemReplace() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); // create item StringStringMap attr1{ @@ -880,38 +949,38 @@ void TestGuiFdoSecrets::testItemReplace() }; auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false); - QVERIFY(item1); + VERIFY(item1); auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false); - QVERIFY(item2); + VERIFY(item2); { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 2); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())}; + COMPARE(QSet::fromList(unlocked), expected); } - QSignalSpy spyItemCreated(&coll->dbusAdaptor(), SIGNAL(ItemCreated(QDBusObjectPath))); - QVERIFY(spyItemCreated.isValid()); - QSignalSpy spyItemChanged(&coll->dbusAdaptor(), SIGNAL(ItemChanged(QDBusObjectPath))); - QVERIFY(spyItemChanged.isValid()); + QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath))); + VERIFY(spyItemCreated.isValid()); + QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath))); + VERIFY(spyItemChanged.isValid()); { // when replace, existing item with matching attr is updated auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true); - QVERIFY(item3); - QCOMPARE(item2, item3); - COMPARE_DBUS_LOCAL_CALL(item3->label(), QStringLiteral("abc3")); + VERIFY(item3); + COMPARE(item2->path(), item3->path()); + DBUS_COMPARE(item3->label(), QStringLiteral("abc3")); // there are still 2 entries - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 2); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())}; + COMPARE(QSet::fromList(unlocked), expected); - QCOMPARE(spyItemCreated.count(), 0); + QTRY_COMPARE(spyItemCreated.count(), 0); // there may be multiple changed signals, due to each item attribute is set separately - QVERIFY(!spyItemChanged.isEmpty()); + QTRY_VERIFY(!spyItemChanged.isEmpty()); for (const auto& args : spyItemChanged) { - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item3->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item3->path()); } } @@ -920,221 +989,321 @@ void TestGuiFdoSecrets::testItemReplace() { // when NOT replace, another entry is created auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false); - QVERIFY(item4); - COMPARE_DBUS_LOCAL_CALL(item2->label(), QStringLiteral("abc3")); - COMPARE_DBUS_LOCAL_CALL(item4->label(), QStringLiteral("abc4")); + VERIFY(item4); + DBUS_COMPARE(item2->label(), QStringLiteral("abc3")); + DBUS_COMPARE(item4->label(), QStringLiteral("abc4")); // there are 3 entries - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 3); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{ + QDBusObjectPath(item1->path()), + QDBusObjectPath(item2->path()), + QDBusObjectPath(item4->path()), + }; + COMPARE(QSet::fromList(unlocked), expected); - QCOMPARE(spyItemCreated.count(), 1); + QTRY_COMPARE(spyItemCreated.count(), 1); { auto args = spyItemCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item4->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item4->path()); } // there may be multiple changed signals, due to each item attribute is set separately - QVERIFY(!spyItemChanged.isEmpty()); + VERIFY(!spyItemChanged.isEmpty()); for (const auto& args : spyItemChanged) { - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item4->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item4->path()); } } } +void TestGuiFdoSecrets::testItemReplaceExistingLocked() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + + // create item + StringStringMap attr1{ + {"application", "fdosecrets-test"}, + {"attr-i[bute]", "![some] -value*"}, + {"fdosecrets-attr", "1"}, + }; + + auto item = createItem(sess, coll, "abc1", "Password", attr1, false); + VERIFY(item); + + // make sure the item is locked + { + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + FdoSecrets::settings()->setConfirmAccessItem(true); + m_client->setItemAuthorized(entry->uuid(), AuthDecision::Undecided); + DBUS_COMPARE(item->locked(), true); + } + + // when replace with a locked item, there will be an prompt + auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true); + VERIFY(item2); + COMPARE(item2->path(), item->path()); + DBUS_COMPARE(item2->label(), QStringLiteral("abc2")); +} + void TestGuiFdoSecrets::testItemSecret() { const QString TEXT_PLAIN = "text/plain"; const QString APPLICATION_OCTET_STREAM = "application/octet-stream"; auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); + + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); // plain text secret { - CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); - auto ss = m_cipher->decrypt(encrypted); - QCOMPARE(ss.contentType, TEXT_PLAIN); - QCOMPARE(ss.value, item->backend()->password().toUtf8()); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); } - // get secret with notification (only works when called from DBUS) + // get secret with notification FdoSecrets::settings()->setShowNotification(true); { QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int))); - QVERIFY(spyShowNotification.isValid()); + VERIFY(spyShowNotification.isValid()); - auto iitem = interfaceOf(item); - QVERIFY(static_cast(iitem)); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); - auto replyMsg = iitem->call(QDBus::BlockWithGui, "GetSecret", QVariant::fromValue(sess->objectPath())); - auto reply = QDBusPendingReply(replyMsg); - QVERIFY(reply.isValid()); - auto ss = m_cipher->decrypt(reply.argumentAt<0>()); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); - QCOMPARE(ss.contentType, TEXT_PLAIN); - QCOMPARE(ss.value, item->backend()->password().toUtf8()); - - QCOMPARE(spyShowNotification.count(), 1); + QTRY_COMPARE(spyShowNotification.count(), 1); } FdoSecrets::settings()->setShowNotification(false); // set secret with plain text { - SecretStruct ss; + // first create Secret in wire format, + // then convert to internal format and encrypt + // finally convert encrypted internal format back to wire format to pass to SetSecret + wire::Secret ss; ss.contentType = TEXT_PLAIN; ss.value = "NewPassword"; - ss.session = sess->objectPath(); - QVERIFY(!item->setSecret(m_cipher->encrypt(ss)).isError()); + ss.session = QDBusObjectPath(sess->path()); + auto encrypted = m_cipher->encrypt(ss.unmarshal(m_plugin->dbus())); + DBUS_VERIFY(item->SetSecret(encrypted.marshal())); - QCOMPARE(item->backend()->password().toUtf8(), ss.value); + COMPARE(entry->password().toUtf8(), ss.value); } // set secret with something else is saved as attachment { - SecretStruct expected; + wire::Secret expected; expected.contentType = APPLICATION_OCTET_STREAM; - expected.value = "NewPasswordBinary"; - expected.session = sess->objectPath(); - QVERIFY(!item->setSecret(m_cipher->encrypt(expected)).isError()); + expected.value = QByteArrayLiteral("NewPasswordBinary"); + expected.session = QDBusObjectPath(sess->path()); + DBUS_VERIFY(item->SetSecret(m_cipher->encrypt(expected.unmarshal(m_plugin->dbus())).marshal())); - QCOMPARE(item->backend()->password(), QStringLiteral("")); + COMPARE(entry->password(), QStringLiteral("")); - CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); - auto ss = m_cipher->decrypt(encrypted); - QCOMPARE(ss.contentType, expected.contentType); - QCOMPARE(ss.value, expected.value); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, expected.contentType); + COMPARE(ss.value, expected.value); } } void TestGuiFdoSecrets::testItemDelete() { - FdoSecrets::settings()->setNoConfirmDeleteItem(false); + FdoSecrets::settings()->setConfirmDeleteItem(true); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); // save the path which will be gone after the deletion. - auto itemPath = item->objectPath(); + auto itemPath = item->path(); - QSignalSpy spyItemDeleted(&coll->dbusAdaptor(), SIGNAL(ItemDeleted(QDBusObjectPath))); - QVERIFY(spyItemDeleted.isValid()); + QSignalSpy spyItemDeleted(coll.data(), SIGNAL(ItemDeleted(QDBusObjectPath))); + VERIFY(spyItemDeleted.isValid()); - CHECKED_DBUS_LOCAL_CALL(prompt, item->deleteItem()); - QVERIFY(prompt); + DBUS_GET(promptPath, item->Delete()); + auto prompt = getProxy(promptPath); + VERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save - if (item->isDeletePermanent()) { - MessageBox::setNextAnswer(MessageBox::Delete); - } else { - MessageBox::setNextAnswer(MessageBox::Move); - } - QVERIFY(!prompt->prompt("").isError()); - + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + MessageBox::setNextAnswer(MessageBox::Delete); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).toString(), QStringLiteral("")); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).toString(), QStringLiteral("")); - QCOMPARE(spyItemDeleted.count(), 1); + QTRY_COMPARE(spyItemDeleted.count(), 1); + args = spyItemDeleted.takeFirst(); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), itemPath); +} + +void TestGuiFdoSecrets::testItemLockState() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + + auto secret = + wire::Secret{ + QDBusObjectPath(sess->path()), + {}, + "NewPassword", + "text/plain", + } + .unmarshal(m_plugin->dbus()); + auto encrypted = m_cipher->encrypt(secret).marshal(); + + // when access confirmation is disabled, item is unlocked when the collection is unlocked + FdoSecrets::settings()->setConfirmAccessItem(false); + DBUS_COMPARE(item->locked(), false); + + // when access confirmation is enabled, item is locked if the client has no authorization + FdoSecrets::settings()->setConfirmAccessItem(true); + DBUS_COMPARE(item->locked(), true); + // however, item properties are still accessible as long as the collection is unlocked + DBUS_VERIFY(item->attributes()); + DBUS_VERIFY(item->setAttributes({})); + DBUS_VERIFY(item->label()); + DBUS_VERIFY(item->setLabel("abc")); + DBUS_VERIFY(item->created()); + DBUS_VERIFY(item->modified()); + // except secret, which is locked { - args = spyItemDeleted.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), itemPath); + auto reply = item->GetSecret(QDBusObjectPath(sess->path())); + VERIFY(reply.isError()); + COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED); } + { + auto reply = item->SetSecret(encrypted); + VERIFY(reply.isError()); + COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED); + } + + // item is unlocked if the client is authorized + m_client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed); + DBUS_COMPARE(item->locked(), false); + DBUS_VERIFY(item->GetSecret(QDBusObjectPath(sess->path()))); + DBUS_VERIFY(item->SetSecret(encrypted)); } void TestGuiFdoSecrets::testAlias() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); // read default alias - CHECKED_DBUS_LOCAL_CALL(coll, service->readAlias("default")); - QVERIFY(coll); + DBUS_GET(collPath, service->ReadAlias("default")); + auto coll = getProxy(collPath); + VERIFY(coll); // set extra alias - QVERIFY(!service->setAlias("another", coll).isError()); + DBUS_VERIFY(service->SetAlias("another", QDBusObjectPath(collPath))); // get using extra alias - CHECKED_DBUS_LOCAL_CALL(coll2, service->readAlias("another")); - QVERIFY(coll2); - QCOMPARE(coll, coll2); + DBUS_GET(collPath2, service->ReadAlias("another")); + COMPARE(collPath2, collPath); } void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); // one collection, which is default alias auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // after locking, the collection is still there, but locked lockDatabaseInBackend(); coll = getDefaultCollection(service); - QVERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), true); + VERIFY(coll); + DBUS_COMPARE(coll->locked(), true); // unlock the database, the alias and collection is present unlockDatabaseInBackend(); coll = getDefaultCollection(service); - QVERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); + VERIFY(coll); + DBUS_COMPARE(coll->locked(), false); } void TestGuiFdoSecrets::testExposeSubgroup() { auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup"); - QVERIFY(subgroup); + VERIFY(subgroup); FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // exposing subgroup does not expose entries in other groups - auto items = coll->items(); - QVERIFY(!items.isError()); - QList exposedEntries; - for (const auto& item : items.value()) { - exposedEntries << item->backend(); + DBUS_GET(itemPaths, coll->items()); + QSet exposedEntries; + for (const auto& itemPath : itemPaths) { + exposedEntries << m_plugin->dbus()->pathToObject(itemPath)->backend(); } - QCOMPARE(exposedEntries, subgroup->entries()); + COMPARE(exposedEntries, QSet::fromList(subgroup->entries())); } void TestGuiFdoSecrets::testModifyingExposedGroup() { // test when exposed group is removed the collection is not exposed anymore auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking"); - QVERIFY(subgroup); + VERIFY(subgroup); FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); auto service = enableService(); - QVERIFY(service); + VERIFY(service); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 1); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths.size(), 1); } m_db->metadata()->setRecycleBinEnabled(true); @@ -1142,102 +1311,19 @@ void TestGuiFdoSecrets::testModifyingExposedGroup() QApplication::processEvents(); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 0); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths, {}); } // test setting another exposed group, the collection will be exposed again FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); QApplication::processEvents(); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 1); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths.size(), 1); } } -QPointer TestGuiFdoSecrets::enableService() -{ - FdoSecrets::settings()->setEnabled(true); - VERIFY(m_plugin); - m_plugin->updateServiceState(); - return m_plugin->serviceInstance(); -} - -QPointer TestGuiFdoSecrets::openSession(Service* service, const QString& algo) -{ - // open session has to be called actually over DBUS to get peer info - - VERIFY(service); - auto iservice = interfaceOf(service); - VERIFY(iservice); - - if (algo == PlainCipher::Algorithm) { - auto replyMsg = iservice->call(QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(""))); - auto reply = QDBusPendingReply(replyMsg); - - VERIFY(reply.isValid()); - return FdoSecrets::pathToObject(reply.argumentAt<1>()); - } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { - - DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)), - MpiFromBytes(MpiToBytes(m_serverPublic))); - - auto replyMsg = iservice->call( - QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(m_cipher->m_publicKey))); - auto reply = QDBusPendingReply(replyMsg); - VERIFY(reply.isValid()); - COMPARE(qvariant_cast(reply.argumentAt<0>().variant()), MpiToBytes(m_serverPublic)); - return FdoSecrets::pathToObject(reply.argumentAt<1>()); - } - FAIL("Unsupported algorithm"); -} - -QPointer TestGuiFdoSecrets::getDefaultCollection(Service* service) -{ - VERIFY(service); - auto coll = service->readAlias("default"); - VERIFY(!coll.isError()); - return coll.value(); -} - -QPointer TestGuiFdoSecrets::getFirstItem(Collection* coll) -{ - VERIFY(coll); - auto items = coll->items(); - VERIFY(!items.isError()); - VERIFY(!items.value().isEmpty()); - return items.value().at(0); -} - -QPointer TestGuiFdoSecrets::createItem(Session* sess, - Collection* coll, - const QString& label, - const QString& pass, - const StringStringMap& attr, - bool replace) -{ - VERIFY(sess); - VERIFY(coll); - - QVariantMap properties{ - {DBUS_INTERFACE_SECRET_ITEM ".Label", QVariant::fromValue(label)}, - {DBUS_INTERFACE_SECRET_ITEM ".Attributes", QVariant::fromValue(attr)}, - }; - - SecretStruct ss; - ss.session = sess->objectPath(); - ss.value = pass.toLocal8Bit(); - ss.contentType = "plain/text"; - ss = m_cipher->encrypt(ss); - - PromptBase* prompt = nullptr; - auto item = coll->createItem(properties, ss, replace, prompt); - VERIFY(!item.isError()); - // creating item does not have a prompt to show - VERIFY(!prompt); - return item.value(); -} - void TestGuiFdoSecrets::lockDatabaseInBackend() { m_dbWidget->lock(); @@ -1251,3 +1337,127 @@ void TestGuiFdoSecrets::unlockDatabaseInBackend() m_db = m_dbWidget->database(); QApplication::processEvents(); } + +// the following functions have return value, switch macros to the version supporting that +#undef VERIFY +#undef VERIFY2 +#undef COMPARE +#define VERIFY(stmt) VERIFY2_RET(stmt, "") +#define VERIFY2 VERIFY2_RET +#define COMPARE COMPARE_RET + +QSharedPointer TestGuiFdoSecrets::enableService() +{ + FdoSecrets::settings()->setEnabled(true); + VERIFY(m_plugin); + m_plugin->updateServiceState(); + return getProxy(QDBusObjectPath(DBUS_PATH_SECRETS)); +} + +QSharedPointer TestGuiFdoSecrets::openSession(const QSharedPointer& service, + const QString& algo) +{ + VERIFY(service); + + if (algo == PlainCipher::Algorithm) { + DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(""))); + + return getProxy(sessPath); + } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { + + DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)), + MpiFromBytes(MpiToBytes(m_serverPublic))); + + DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(m_cipher->m_publicKey))); + + COMPARE(qvariant_cast(output.variant()), MpiToBytes(m_serverPublic)); + return getProxy(sessPath); + } + QTest::qFail("Unsupported algorithm", __FILE__, __LINE__); + return {}; +} + +QSharedPointer TestGuiFdoSecrets::getDefaultCollection(const QSharedPointer& service) +{ + VERIFY(service); + DBUS_GET(collPath, service->ReadAlias("default")); + return getProxy(collPath); +} + +QSharedPointer TestGuiFdoSecrets::getFirstItem(const QSharedPointer& coll) +{ + VERIFY(coll); + DBUS_GET(itemPaths, coll->items()); + VERIFY(!itemPaths.isEmpty()); + return getProxy(itemPaths.first()); +} + +QSharedPointer TestGuiFdoSecrets::createItem(const QSharedPointer& sess, + const QSharedPointer& coll, + const QString& label, + const QString& pass, + const StringStringMap& attr, + bool replace, + bool expectPrompt) +{ + VERIFY(sess); + VERIFY(coll); + + QVariantMap properties{ + {DBUS_INTERFACE_SECRET_ITEM + ".Label", QVariant::fromValue(label)}, + {DBUS_INTERFACE_SECRET_ITEM + ".Attributes", QVariant::fromValue(attr)}, + }; + + wire::Secret ss; + ss.session = QDBusObjectPath(sess->path()); + ss.value = pass.toLocal8Bit(); + ss.contentType = "plain/text"; + auto encrypted = m_cipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal(); + + DBUS_GET2(itemPath, promptPath, coll->CreateItem(properties, encrypted, replace)); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + bool found = driveAccessControlDialog(); + COMPARE(found, expectPrompt); + + // wait for signal + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + itemPath = getSignalVariantArgument(args.at(1)); + + return getProxy(itemPath); +} + +bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember) +{ + QApplication::processEvents(); + for (auto w : qApp->allWidgets()) { + if (!w->isWindow()) { + continue; + } + auto dlg = qobject_cast(w); + if (dlg) { + auto rememberCheck = dlg->findChild("rememberCheck"); + VERIFY(rememberCheck); + rememberCheck->setChecked(remember); + QTest::keyClick(dlg, Qt::Key_Enter); + QApplication::processEvents(); + return true; + } + } + return false; +} + +#undef VERIFY +#define VERIFY QVERIFY +#undef COMPARE +#define COMPARE QCOMPARE diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h index 84f7147e..8ded8658 100644 --- a/tests/gui/TestGuiFdoSecrets.h +++ b/tests/gui/TestGuiFdoSecrets.h @@ -19,14 +19,14 @@ #define KEEPASSXC_TESTGUIFDOSECRETS_H #include -#include +#include #include #include #include #include #include "fdosecrets/GcryptMPI.h" -#include "fdosecrets/objects/DBusTypes.h" +#include "fdosecrets/dbus/DBusTypes.h" class MainWindow; class Database; @@ -42,7 +42,13 @@ namespace FdoSecrets class Item; class Prompt; class DhIetf1024Sha256Aes128CbcPkcs7; + class DBusClient; } // namespace FdoSecrets +class ServiceProxy; +class CollectionProxy; +class ItemProxy; +class SessionProxy; +class PromptProxy; class QAbstractItemView; @@ -59,12 +65,11 @@ private slots: void cleanup(); void cleanupTestCase(); - void testDBusSpec(); - void testServiceEnable(); void testServiceEnableNoExposedDatabase(); void testServiceSearch(); void testServiceUnlock(); + void testServiceUnlockItems(); void testServiceLock(); void testSessionOpen(); @@ -72,11 +77,15 @@ private slots: void testCollectionCreate(); void testCollectionDelete(); + void testCollectionChange(); void testItemCreate(); + void testItemChange(); void testItemReplace(); + void testItemReplaceExistingLocked(); void testItemSecret(); void testItemDelete(); + void testItemLockState(); void testAlias(); void testDefaultAliasAlwaysPresent(); @@ -88,21 +97,38 @@ private slots: void testDuplicateName(); protected slots: - void createDatabaseCallback(); + void driveNewDatabaseWizard(); + bool driveAccessControlDialog(bool remember = true); private: void lockDatabaseInBackend(); void unlockDatabaseInBackend(); - QPointer enableService(); - QPointer openSession(FdoSecrets::Service* service, const QString& algo); - QPointer getDefaultCollection(FdoSecrets::Service* service); - QPointer getFirstItem(FdoSecrets::Collection* coll); - QPointer createItem(FdoSecrets::Session* sess, - FdoSecrets::Collection* coll, - const QString& label, - const QString& pass, - const StringStringMap& attr, - bool replace); + QSharedPointer enableService(); + QSharedPointer openSession(const QSharedPointer& service, const QString& algo); + QSharedPointer getDefaultCollection(const QSharedPointer& service); + QSharedPointer getFirstItem(const QSharedPointer& coll); + QSharedPointer createItem(const QSharedPointer& sess, + const QSharedPointer& coll, + const QString& label, + const QString& pass, + const FdoSecrets::wire::StringStringMap& attr, + bool replace, + bool expectPrompt = false); + template QSharedPointer getProxy(const QDBusObjectPath& path) const + { + auto ret = QSharedPointer{ + new Proxy(QStringLiteral("org.freedesktop.secrets"), path.path(), QDBusConnection::sessionBus())}; + if (!ret->isValid()) { + return {}; + } + return ret; + } + + template T getSignalVariantArgument(const QVariant& arg) + { + const auto& in = arg.value().variant(); + return qdbus_cast(in); + } private: QScopedPointer m_mainWindow; @@ -111,6 +137,7 @@ private: QSharedPointer m_db; QPointer m_plugin; + QSharedPointer m_client; // For DH session tests GcryptMPI m_serverPrivate; diff --git a/tests/util/FdoSecretsProxy.cpp b/tests/util/FdoSecretsProxy.cpp new file mode 100644 index 00000000..b48f28c5 --- /dev/null +++ b/tests/util/FdoSecretsProxy.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FdoSecretsProxy.h" + +#define IMPL_PROXY(name) \ + name##Proxy::name##Proxy( \ + const QString& service, const QString& path, const QDBusConnection& connection, QObject* parent) \ + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) \ + { \ + } \ + name##Proxy::~name##Proxy() = default; + +IMPL_PROXY(Service) +IMPL_PROXY(Collection) +IMPL_PROXY(Item) +IMPL_PROXY(Session) +IMPL_PROXY(Prompt) + +#undef IMPL_PROXY diff --git a/tests/util/FdoSecretsProxy.h b/tests/util/FdoSecretsProxy.h new file mode 100644 index 00000000..c8bcafb7 --- /dev/null +++ b/tests/util/FdoSecretsProxy.h @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2020 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETSPROXY_H +#define KEEPASSXC_FDOSECRETSPROXY_H + +#include "fdosecrets/dbus/DBusTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/** + * Mimic the interface of QDBusPendingReply so the same code can be used in test + */ +template class PropertyReply +{ + QDBusPendingReply m_reply; + +public: + /*implicit*/ PropertyReply(const QDBusMessage& reply) + : m_reply(reply) + { + } + bool isFinished() const + { + return m_reply.isFinished(); + } + bool isValid() const + { + return m_reply.isValid(); + } + bool isError() const + { + return m_reply.isError(); + } + QDBusError error() const + { + return m_reply.error(); + } + T value() const + { + return qdbus_cast(m_reply.value().variant()); + } + template T argumentAt() const + { + return value(); + } +}; + +#define IMPL_GET_PROPERTY(name) \ + QDBusMessage msg = QDBusMessage::createMethodCall( \ + service(), path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get")); \ + msg << interface() << QStringLiteral(#name); \ + return \ + { \ + connection().call(msg, QDBus::BlockWithGui) \ + } + +#define IMPL_SET_PROPERTY(name, value) \ + QDBusMessage msg = QDBusMessage::createMethodCall( \ + service(), path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set")); \ + msg << interface() << QStringLiteral(#name) << QVariant::fromValue(QDBusVariant(QVariant::fromValue(value))); \ + return \ + { \ + connection().call(msg, QDBus::BlockWithGui) \ + } + +/* + * Proxy class for interface org.freedesktop.Secret.Service + */ +class ServiceProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Service"; + } + +public: + ServiceProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~ServiceProxy() override; + + inline PropertyReply> collections() const + { + IMPL_GET_PROPERTY(Collections); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply CreateCollection(const QVariantMap& properties, + const QString& alias) + { + QList argumentList; + argumentList << QVariant::fromValue(properties) << QVariant::fromValue(alias); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("CreateCollection"), argumentList)}; + } + + inline QDBusPendingReply GetSecrets(const QList& items, + const QDBusObjectPath& session) + { + QList argumentList; + argumentList << QVariant::fromValue(items) << QVariant::fromValue(session); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("GetSecrets"), argumentList)}; + } + + inline QDBusPendingReply, QDBusObjectPath> Lock(const QList& paths) + { + QList argumentList; + argumentList << QVariant::fromValue(paths); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Lock"), argumentList)}; + } + inline QDBusPendingReply OpenSession(const QString& algorithm, + const QDBusVariant& input) + { + QList argumentList; + argumentList << QVariant::fromValue(algorithm) << QVariant::fromValue(input); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("OpenSession"), argumentList)}; + } + inline QDBusPendingReply ReadAlias(const QString& name) + { + QList argumentList; + argumentList << QVariant::fromValue(name); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("ReadAlias"), argumentList)}; + } + + inline QDBusPendingReply, QList> + SearchItems(FdoSecrets::wire::StringStringMap attributes) + { + QList argumentList; + argumentList << QVariant::fromValue(attributes); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SearchItems"), argumentList)}; + } + inline QDBusPendingReply<> SetAlias(const QString& name, const QDBusObjectPath& collection) + { + QList argumentList; + argumentList << QVariant::fromValue(name) << QVariant::fromValue(collection); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SetAlias"), argumentList)}; + } + + inline QDBusPendingReply, QDBusObjectPath> Unlock(const QList& paths) + { + QList argumentList; + argumentList << QVariant::fromValue(paths); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Unlock"), argumentList)}; + } +Q_SIGNALS: // SIGNALS + void CollectionChanged(const QDBusObjectPath& collection); + void CollectionCreated(const QDBusObjectPath& collection); + void CollectionDeleted(const QDBusObjectPath& collection); +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Collection + */ +class CollectionProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Collection"; + } + +public: + CollectionProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~CollectionProxy() override; + + inline PropertyReply created() const + { + IMPL_GET_PROPERTY(Created); + } + + inline PropertyReply> items() const + { + IMPL_GET_PROPERTY(Items); + } + + inline PropertyReply label() const + { + IMPL_GET_PROPERTY(Label); + } + inline QDBusPendingReply<> setLabel(const QString& value) + { + IMPL_SET_PROPERTY(Label, value); + } + + inline PropertyReply locked() const + { + IMPL_GET_PROPERTY(Locked); + } + + inline PropertyReply modified() const + { + IMPL_GET_PROPERTY(Modified); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply + CreateItem(const QVariantMap& properties, FdoSecrets::wire::Secret secret, bool replace) + { + QList argumentList; + argumentList << QVariant::fromValue(properties) << QVariant::fromValue(secret) << QVariant::fromValue(replace); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("CreateItem"), argumentList)}; + } + inline QDBusPendingReply Delete() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Delete"), argumentList)}; + } + + inline QDBusPendingReply> SearchItems(FdoSecrets::wire::StringStringMap attributes) + { + QList argumentList; + argumentList << QVariant::fromValue(attributes); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SearchItems"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS + void ItemChanged(const QDBusObjectPath& item); + void ItemCreated(const QDBusObjectPath& item); + void ItemDeleted(const QDBusObjectPath& item); +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Item + */ +class ItemProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Item"; + } + +public: + ItemProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~ItemProxy() override; + + inline PropertyReply attributes() const + { + IMPL_GET_PROPERTY(Attributes); + } + inline QDBusPendingReply<> setAttributes(FdoSecrets::wire::StringStringMap value) + { + IMPL_SET_PROPERTY(Attributes, value); + } + + inline PropertyReply created() const + { + IMPL_GET_PROPERTY(Created); + } + + inline PropertyReply label() const + { + IMPL_GET_PROPERTY(Label); + } + inline QDBusPendingReply<> setLabel(const QString& value) + { + IMPL_SET_PROPERTY(Label, value); + } + + inline PropertyReply locked() const + { + IMPL_GET_PROPERTY(Locked); + } + + inline PropertyReply modified() const + { + IMPL_GET_PROPERTY(Modified); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply Delete() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Delete"), argumentList)}; + } + + inline QDBusPendingReply GetSecret(const QDBusObjectPath& session) + { + QList argumentList; + argumentList << QVariant::fromValue(session); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("GetSecret"), argumentList)}; + } + + inline QDBusPendingReply<> SetSecret(FdoSecrets::wire::Secret secret) + { + QList argumentList; + argumentList << QVariant::fromValue(secret); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SetSecret"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Session + */ +class SessionProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Session"; + } + +public: + SessionProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~SessionProxy() override; + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> Close() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Close"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Prompt + */ +class PromptProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Prompt"; + } + +public: + PromptProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~PromptProxy() override; + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> Dismiss() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Dismiss"), argumentList)}; + } + + inline QDBusPendingReply<> Prompt(const QString& windowId) + { + QList argumentList; + argumentList << QVariant::fromValue(windowId); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Prompt"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS + void Completed(bool dismissed, const QDBusVariant& result); +}; + +#undef IMPL_GET_PROPERTY +#undef IMPL_SET_PROPERTY + +#endif // KEEPASSXC_FDOSECRETSPROXY_H