diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 71529937c..ee955050a 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -7880,11 +7880,27 @@ Please consider generating a new key file. - <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 - Confirm when clients request entry deletion + <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> + + + + + <html><head/><body><p>This improves compatibility with certain applications + which search for password without unlocking the database first.</p><p>But enabling this may also + crash the client if the database can not be unlocked within a certain timeout. (Usually 25s, but may be a + different value set in applications.)</p></body></html> + + + + + Prompt to unlock database before searching diff --git a/src/core/Config.cpp b/src/core/Config.cpp index b586cee05..54fc59c81 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -178,6 +178,7 @@ static const QHash configStrings = { {Config::FdoSecrets_ShowNotification, {QS("FdoSecrets/ShowNotification"), Roaming, true}}, {Config::FdoSecrets_ConfirmDeleteItem, {QS("FdoSecrets/ConfirmDeleteItem"), Roaming, true}}, {Config::FdoSecrets_ConfirmAccessItem, {QS("FdoSecrets/ConfirmAccessItem"), Roaming, true}}, + {Config::FdoSecrets_UnlockBeforeSearch, {QS("FdoSecrets/UnlockBeforeSearch"), Roaming, true}}, // KeeShare {Config::KeeShare_QuietSuccess, {QS("KeeShare/QuietSuccess"), Roaming, false}}, diff --git a/src/core/Config.h b/src/core/Config.h index c0e3d142b..8e5c14e35 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -155,6 +155,7 @@ public: FdoSecrets_ShowNotification, FdoSecrets_ConfirmDeleteItem, FdoSecrets_ConfirmAccessItem, + FdoSecrets_UnlockBeforeSearch, KeeShare_QuietSuccess, KeeShare_Own, diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp index 58126733a..2f1e95182 100644 --- a/src/fdosecrets/FdoSecretsSettings.cpp +++ b/src/fdosecrets/FdoSecretsSettings.cpp @@ -83,6 +83,16 @@ namespace FdoSecrets config()->set(Config::FdoSecrets_ConfirmAccessItem, confirmAccessItem); } + bool FdoSecretsSettings::unlockBeforeSearch() const + { + return config()->get(Config::FdoSecrets_UnlockBeforeSearch).toBool(); + } + + void FdoSecretsSettings::setUnlockBeforeSearch(bool unlockBeforeSearch) + { + config()->set(Config::FdoSecrets_UnlockBeforeSearch, unlockBeforeSearch); + } + QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer& db) const { return exposedGroup(db.data()); diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h index 24a37a8d2..31ab005f6 100644 --- a/src/fdosecrets/FdoSecretsSettings.h +++ b/src/fdosecrets/FdoSecretsSettings.h @@ -44,6 +44,9 @@ namespace FdoSecrets bool confirmAccessItem() const; void setConfirmAccessItem(bool confirmAccessItem); + bool unlockBeforeSearch() const; + void setUnlockBeforeSearch(bool unlockBeforeSearch); + // Per db settings QUuid exposedGroup(const QSharedPointer& db) const; diff --git a/src/fdosecrets/dbus/DBusMgr.cpp b/src/fdosecrets/dbus/DBusMgr.cpp index e60c068aa..1a4b2b73c 100644 --- a/src/fdosecrets/dbus/DBusMgr.cpp +++ b/src/fdosecrets/dbus/DBusMgr.cpp @@ -177,14 +177,12 @@ namespace FdoSecrets // It's still weak and if the application does a prctl(PR_SET_DUMPABLE, 0) this link cannot be accessed. QFileInfo exe(QStringLiteral("/proc/%1/exe").arg(pid)); info.exePath = exe.canonicalFilePath(); - qDebug() << "process" << pid << info.exePath; // /proc/pid/cmdline gives full command line QFile cmdline(QStringLiteral("/proc/%1/cmdline").arg(pid)); if (cmdline.open(QFile::ReadOnly)) { info.command = QString::fromLocal8Bit(cmdline.readAll().replace('\0', ' ')).trimmed(); } - qDebug() << "process" << pid << info.command; // /proc/pid/stat gives ppid, name QFile stat(QStringLiteral("/proc/%1/stat").arg(pid)); diff --git a/src/fdosecrets/dbus/DBusObject.h b/src/fdosecrets/dbus/DBusObject.h index d2314438d..8984cf17e 100644 --- a/src/fdosecrets/dbus/DBusObject.h +++ b/src/fdosecrets/dbus/DBusObject.h @@ -85,7 +85,7 @@ namespace FdoSecrets // Implicitly convert from QDBusError DBusResult(QDBusError::ErrorType error) // NOLINT(google-explicit-constructor) - : QString(QDBusError::errorString(error)) + : QString(error == QDBusError::ErrorType::NoError ? "" : QDBusError::errorString(error)) { } diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp index c4e24839b..610a4b99e 100644 --- a/src/fdosecrets/objects/Collection.cpp +++ b/src/fdosecrets/objects/Collection.cpp @@ -24,7 +24,9 @@ #include "core/Tools.h" #include "gui/DatabaseWidget.h" +#include "gui/GuiTools.h" +#include #include namespace FdoSecrets @@ -62,9 +64,9 @@ namespace FdoSecrets // delete all items // this has to be done because the backend is actually still there, just we don't expose them - // NOTE: Do NOT use a for loop, because Item::doDelete will remove itself from m_items. + // NOTE: Do NOT use a for loop, because Item::removeFromDBus will remove itself from m_items. while (!m_items.isEmpty()) { - m_items.first()->doDelete(); + m_items.first()->removeFromDBus(); } cleanupConnections(); dbus()->unregisterObject(this); @@ -91,7 +93,7 @@ namespace FdoSecrets void Collection::reloadBackendOrDelete() { if (!reloadBackend()) { - doDelete(); + removeFromDBus(); } } @@ -194,31 +196,22 @@ namespace FdoSecrets return {}; } - DBusResult Collection::remove(const DBusClientPtr& client, PromptBase*& prompt) + DBusResult Collection::remove(PromptBase*& prompt) { auto ret = ensureBackend(); if (ret.err()) { return ret; } - // Delete means close database prompt = PromptBase::Create(service(), this); if (!prompt) { return QDBusError::InternalError; } - if (backendLocked()) { - // this won't raise a dialog, immediate execute - ret = prompt->prompt(client, {}); - if (ret.err()) { - return ret; - } - prompt = nullptr; - } - // defer the close to the prompt return {}; } - DBusResult Collection::searchItems(const StringStringMap& attributes, QList& items) + DBusResult + Collection::searchItems(const DBusClientPtr& client, const StringStringMap& attributes, QList& items) { items.clear(); @@ -226,8 +219,26 @@ namespace FdoSecrets if (ret.err()) { return ret; } - ret = ensureUnlocked(); - if (ret.err()) { + + if (backendLocked() && settings()->unlockBeforeSearch()) { + // do a blocking unlock prompt first. + // while technically not correct, this should improve compatibility. + // see issue #4443 + auto prompt = PromptBase::Create(service(), QSet{this}, QSet{}); + if (!prompt) { + return QDBusError::InternalError; + } + // we don't know the windowId to show the prompt correctly, + // but the default of showing over the KPXC main window should be good enough + ret = prompt->prompt(client, ""); + // blocking wait + QEventLoop loop; + connect(prompt, &PromptBase::completed, &loop, &QEventLoop::quit); + loop.exec(); + } + + // check again because the prompt may be cancelled + if (backendLocked()) { // 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. @@ -258,7 +269,11 @@ namespace FdoSecrets terms << attributeToTerm(it.key(), it.value()); } - const auto foundEntries = EntrySearcher(false, true).search(terms, m_exposedGroup); + constexpr auto caseSensitive = false; + constexpr auto skipProtected = true; + constexpr auto forceSearch = true; + const auto foundEntries = + EntrySearcher(caseSensitive, skipProtected).search(terms, m_exposedGroup, forceSearch); items.reserve(foundEntries.size()); for (const auto& entry : foundEntries) { items << m_entryToItem.value(entry); @@ -298,36 +313,10 @@ namespace FdoSecrets if (ret.err()) { return ret; } - ret = ensureUnlocked(); - if (ret.err()) { - return ret; - } item = nullptr; - QString itemPath; - auto iterAttr = properties.find(DBUS_INTERFACE_SECRET_ITEM + ".Attributes"); - if (iterAttr != properties.end()) { - // 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 - QList existing; - ret = searchItems(attributes, existing); - if (ret.err()) { - return ret; - } - if (!existing.isEmpty() && replace) { - item = existing.front(); - } - } - - prompt = PromptBase::Create(service(), this, properties, secret, itemPath, item); + prompt = PromptBase::Create(service(), this, properties, secret, replace); if (!prompt) { return QDBusError::InternalError; } @@ -418,7 +407,7 @@ namespace FdoSecrets void Collection::onDatabaseLockChanged() { if (!reloadBackend()) { - doDelete(); + removeFromDBus(); return; } emit collectionLockChanged(backendLocked()); @@ -436,7 +425,7 @@ namespace FdoSecrets auto newGroup = m_backend->database()->rootGroup()->findGroupByUuid(newUuid); if (!newGroup || inRecycleBin(newGroup)) { // no exposed group, delete self - doDelete(); + removeFromDBus(); return; } @@ -505,7 +494,7 @@ namespace FdoSecrets // this has to be done because the backend is actually still there // just we don't expose them for (const auto& item : asConst(m_items)) { - item->doDelete(); + item->removeFromDBus(); } // repopulate @@ -527,7 +516,7 @@ namespace FdoSecrets // forward delete signals connect(entry->group(), &Group::entryAboutToRemove, item, [item](Entry* toBeRemoved) { if (item->backend() == toBeRemoved) { - item->doDelete(); + item->removeFromDBus(); } }); @@ -568,17 +557,26 @@ namespace FdoSecrets { Q_ASSERT(m_backend); - return m_backend->lock(); + // do not call m_backend->lock() directly + // let the service handle locking which handles concurrent calls + return service()->doLockDatabase(m_backend); } void Collection::doUnlock() { Q_ASSERT(m_backend); - service()->doUnlockDatabaseInDialog(m_backend); + return service()->doUnlockDatabaseInDialog(m_backend); } - void Collection::doDelete() + bool Collection::doDelete() + { + Q_ASSERT(m_backend); + + return service()->doCloseDatabase(m_backend); + } + + void Collection::removeFromDBus() { if (!m_backend) { // I'm already deleted @@ -637,9 +635,18 @@ namespace FdoSecrets return !m_backend || !m_backend->database()->isInitialized() || m_backend->isLocked(); } - void Collection::doDeleteEntries(QList entries) + bool Collection::doDeleteEntry(Entry* entry) { - m_backend->deleteEntries(std::move(entries), FdoSecrets::settings()->confirmDeleteItem()); + // Confirm entry removal before moving forward + bool permanent = inRecycleBin(entry) || !m_backend->database()->metadata()->recycleBinEnabled(); + if (FdoSecrets::settings()->confirmDeleteItem() + && !GuiTools::confirmDeleteEntries(m_backend, {entry}, permanent)) { + return false; + } + + auto num = GuiTools::deleteEntriesResolveReferences(m_backend, {entry}, permanent); + + return num != 0; } Group* Collection::findCreateGroupByPath(const QString& groupPath) diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h index 690eb0d99..4bd83d152 100644 --- a/src/fdosecrets/objects/Collection.h +++ b/src/fdosecrets/objects/Collection.h @@ -63,8 +63,10 @@ namespace FdoSecrets Q_INVOKABLE DBUS_PROPERTY DBusResult modified(qulonglong& modified) const; - Q_INVOKABLE DBusResult remove(const DBusClientPtr& client, PromptBase*& prompt); - Q_INVOKABLE DBusResult searchItems(const StringStringMap& attributes, QList& items); + Q_INVOKABLE DBusResult remove(PromptBase*& prompt); + Q_INVOKABLE DBusResult searchItems(const DBusClientPtr& client, + const StringStringMap& attributes, + QList& items); Q_INVOKABLE DBusResult createItem(const QVariantMap& properties, const Secret& secret, bool replace, Item*& item, PromptBase*& prompt); @@ -112,14 +114,18 @@ namespace FdoSecrets public slots: // expose some methods for Prompt to use - bool doLock(); - void doUnlock(); - Item* doNewItem(const DBusClientPtr& client, QString itemPath); - // will remove self - void doDelete(); - // delete the Entry in backend from this collection - void doDeleteEntries(QList entries); + bool doLock(); + // actually only closes database tab in KPXC + bool doDelete(); + // Async, finish signal doneUnlockCollection + void doUnlock(); + + bool doDeleteEntry(Entry* entry); + Item* doNewItem(const DBusClientPtr& client, QString itemPath); + + // Only delete from dbus, will remove self. Do not affect database in KPXC + void removeFromDBus(); // force reload info from backend, potentially delete self bool reloadBackend(); diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp index e6c2831b4..9a7d94739 100644 --- a/src/fdosecrets/objects/Item.cpp +++ b/src/fdosecrets/objects/Item.cpp @@ -335,7 +335,14 @@ namespace FdoSecrets return m_backend; } - void Item::doDelete() + bool Item::doDelete() + { + Q_ASSERT(m_backend); + + return collection()->doDeleteEntry(m_backend); + } + + void Item::removeFromDBus() { emit itemAboutToDelete(); diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h index e11cdb88c..8be4e7d27 100644 --- a/src/fdosecrets/objects/Item.h +++ b/src/fdosecrets/objects/Item.h @@ -91,8 +91,13 @@ namespace FdoSecrets QString path() const; public slots: - void doDelete(); + // will actually delete the entry in KPXC + bool doDelete(); + // Only delete from dbus, will remove self. Do not affect database in KPXC + void removeFromDBus(); + + private slots: /** * Check if the backend is a valid object, send error reply if not. * @return No error if the backend is valid. diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp index 9fa06c25e..58fdfa4e1 100644 --- a/src/fdosecrets/objects/Prompt.cpp +++ b/src/fdosecrets/objects/Prompt.cpp @@ -32,6 +32,7 @@ namespace FdoSecrets { + const PromptResult PromptResult::Pending{PromptResult::AsyncPending}; PromptBase::PromptBase(Service* parent) : DBusObject(parent) @@ -60,19 +61,7 @@ namespace FdoSecrets return qobject_cast(parent()); } - DBusResult PromptBase::dismiss() - { - emit completed(true, ""); - return {}; - } - - DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll) - : PromptBase(parent) - , m_collection(coll) - { - } - - DBusResult DeleteCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) + DBusResult PromptBase::prompt(const DBusClientPtr& client, const QString& windowId) { if (thread() != QThread::currentThread()) { DBusResult ret; @@ -81,19 +70,62 @@ namespace FdoSecrets return ret; } - if (!m_collection) { - return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); - } - - MessageBox::OverrideParent override(findWindow(windowId)); - // only need to delete in backend, collection will react itself. - auto accepted = service()->doCloseDatabase(m_collection->backend()); - - emit completed(!accepted, ""); - + QWeakPointer weak = client; + // execute the actual prompt method in event loop to avoid block this method + QTimer::singleShot(0, this, [this, weak, windowId]() { + auto c = weak.lock(); + if (!c) { + return; + } + if (m_signalSent) { + return; + } + auto res = promptSync(c, windowId); + if (!res.isPending()) { + finishPrompt(res.isDismiss()); + } + }); return {}; } + DBusResult PromptBase::dismiss() + { + finishPrompt(true); + return {}; + } + + QVariant PromptBase::currentResult() const + { + return ""; + } + + void PromptBase::finishPrompt(bool dismissed) + { + if (m_signalSent) { + return; + } + m_signalSent = true; + emit completed(dismissed, currentResult()); + } + + DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll) + : PromptBase(parent) + , m_collection(coll) + { + } + + PromptResult DeleteCollectionPrompt::promptSync(const DBusClientPtr&, const QString& windowId) + { + MessageBox::OverrideParent override(findWindow(windowId)); + + // if m_collection is already gone then treat as deletion accepted + auto accepted = true; + if (m_collection) { + accepted = m_collection->doDelete(); + } + return PromptResult::accepted(accepted); + } + CreateCollectionPrompt::CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias) : PromptBase(parent) , m_properties(std::move(properties)) @@ -101,43 +133,45 @@ namespace FdoSecrets { } - DBusResult CreateCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) + QVariant CreateCollectionPrompt::currentResult() const { - if (thread() != QThread::currentThread()) { - DBusResult ret; - QMetaObject::invokeMethod( - this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); - return ret; - } + return QVariant::fromValue(DBusMgr::objectPathSafe(m_coll)); + } + PromptResult CreateCollectionPrompt::promptSync(const DBusClientPtr&, const QString& windowId) + { MessageBox::OverrideParent override(findWindow(windowId)); - auto coll = service()->doNewDatabase(); - if (!coll) { - return dismiss(); - } - - auto ret = coll->setProperties(m_properties); + bool created = false; + // collection with the alias may be created since the prompt was created + auto ret = service()->readAlias(m_alias, m_coll); if (ret.err()) { - coll->doDelete(); - return dismiss(); + return ret; + } + if (!m_coll) { + created = true; + m_coll = service()->doNewDatabase(); + if (!m_coll) { + return PromptResult::accepted(false); + } + } + ret = m_coll->setProperties(m_properties); + if (ret.err()) { + if (created) { + m_coll->removeFromDBus(); + } + return ret; } if (!m_alias.isEmpty()) { - ret = coll->addAlias(m_alias); + ret = m_coll->addAlias(m_alias); if (ret.err()) { - coll->doDelete(); - return dismiss(); + if (created) { + m_coll->removeFromDBus(); + } + return ret; } } - emit completed(false, QVariant::fromValue(coll->objectPath())); - - return {}; - } - - DBusResult CreateCollectionPrompt::dismiss() - { - emit completed(true, QVariant::fromValue(DBusMgr::objectPathSafe(nullptr))); return {}; } @@ -150,35 +184,24 @@ namespace FdoSecrets } } - DBusResult LockCollectionsPrompt::prompt(const DBusClientPtr&, const QString& windowId) + QVariant LockCollectionsPrompt::currentResult() const { - if (thread() != QThread::currentThread()) { - DBusResult ret; - QMetaObject::invokeMethod( - this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); - return ret; - } + return QVariant::fromValue(m_locked); + } + PromptResult LockCollectionsPrompt::promptSync(const DBusClientPtr&, const QString& windowId) + { MessageBox::OverrideParent override(findWindow(windowId)); for (const auto& c : asConst(m_collections)) { if (c) { - if (!c->doLock()) { - return dismiss(); + auto accepted = c->doLock(); + if (accepted) { + m_locked << c->objectPath(); } - m_locked << c->objectPath(); } } - - emit completed(false, QVariant::fromValue(m_locked)); - - return {}; - } - - DBusResult LockCollectionsPrompt::dismiss() - { - emit completed(true, QVariant::fromValue(m_locked)); - return {}; + return PromptResult::accepted(m_locked.size() == m_collections.size()); } UnlockPrompt::UnlockPrompt(Service* parent, const QSet& colls, const QSet& items) @@ -194,15 +217,13 @@ namespace FdoSecrets } } - DBusResult UnlockPrompt::prompt(const DBusClientPtr& client, const QString& windowId) + QVariant UnlockPrompt::currentResult() const { - if (thread() != QThread::currentThread()) { - DBusResult ret; - QMetaObject::invokeMethod( - this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); - return ret; - } + return QVariant::fromValue(m_unlocked); + } + PromptResult UnlockPrompt::promptSync(const DBusClientPtr& client, const QString& windowId) + { MessageBox::OverrideParent override(findWindow(windowId)); // for use in unlockItems @@ -214,6 +235,7 @@ namespace FdoSecrets for (const auto& c : asConst(m_collections)) { if (c) { // doUnlock is nonblocking, execution will continue in collectionUnlockFinished + // it is ok to call doUnlock multiple times before it's actually unlocked by the user c->doUnlock(); waitingForCollections = true; } @@ -222,11 +244,10 @@ namespace FdoSecrets // 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); + unlockItems(); } - return {}; + return PromptResult::Pending; } void UnlockPrompt::collectionUnlockFinished(bool accepted) @@ -248,8 +269,7 @@ namespace FdoSecrets m_unlocked << coll->objectPath(); } else { m_numRejected += 1; - // no longer need to unlock the item if its containing collection - // didn't unlock. + // no longer need to unlock the item if its containing collection didn't unlock. m_items.remove(coll); } @@ -270,7 +290,7 @@ namespace FdoSecrets // flatten to list of entries QList entries; - for (const auto& itemsPerColl : m_items.values()) { + for (const auto& itemsPerColl : asConst(m_items)) { for (const auto& item : itemsPerColl) { if (!item) { m_numRejected += 1; @@ -306,6 +326,7 @@ namespace FdoSecrets auto client = m_client.lock(); if (!client) { // client already gone + qDebug() << "DBus client gone before item unlocking finish"; return; } for (auto it = decisions.constBegin(); it != decisions.constEnd(); ++it) { @@ -326,13 +347,7 @@ namespace FdoSecrets } // 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 {}; + finishPrompt(m_numRejected > 0); } DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item) @@ -341,117 +356,114 @@ namespace FdoSecrets { } - DBusResult DeleteItemPrompt::prompt(const DBusClientPtr&, const QString& windowId) + PromptResult DeleteItemPrompt::promptSync(const DBusClientPtr&, 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)); - // delete item's backend. Item will be notified after the backend is deleted. + // if m_item is gone, assume it's already deleted + bool deleted = true; if (m_item) { - m_item->collection()->doDeleteEntries({m_item->backend()}); + deleted = m_item->doDelete(); } - - emit completed(false, ""); - - return {}; + return PromptResult::accepted(deleted); } CreateItemPrompt::CreateItemPrompt(Service* parent, Collection* coll, QVariantMap properties, Secret secret, - QString itemPath, - Item* existing) + bool replace) : PromptBase(parent) , m_coll(coll) , m_properties(std::move(properties)) , m_secret(std::move(secret)) - , m_itemPath(std::move(itemPath)) - , m_item(existing) + , m_replace(replace) + , m_item(nullptr) // 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) + QVariant CreateItemPrompt::currentResult() const { - if (thread() != QThread::currentThread()) { - DBusResult ret; - QMetaObject::invokeMethod( - this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); - return ret; + return QVariant::fromValue(DBusMgr::objectPathSafe(m_item)); + } + + PromptResult CreateItemPrompt::promptSync(const DBusClientPtr& client, const QString& windowId) + { + if (!m_coll) { + return PromptResult::accepted(false); } - MessageBox::OverrideParent override(findWindow(windowId)); - - if (!m_coll) { - return dismiss(); + bool locked = true; + auto ret = m_coll->locked(locked); + if (locked) { + // collection was locked + return DBusResult{DBUS_ERROR_SECRET_IS_LOCKED}; } // save a weak reference to the client which may be used asynchronously later m_client = client; - // the item doesn't exists yet, create it + // get itemPath to create item and + // try finding an existing item using attributes + QString itemPath{}; + auto iterAttr = m_properties.find(DBUS_INTERFACE_SECRET_ITEM + ".Attributes"); + if (iterAttr != m_properties.end()) { + // 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 + QList existing; + ret = m_coll->searchItems(client, attributes, existing); + if (ret.err()) { + return ret; + } + if (!existing.isEmpty() && m_replace) { + m_item = existing.front(); + } + } + if (!m_item) { - m_item = m_coll->doNewItem(client, m_itemPath); + // the item doesn't exist yet, create it + m_item = m_coll->doNewItem(client, itemPath); if (!m_item) { // may happen if entry somehow ends up in recycle bin - return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); + return DBusResult{DBUS_ERROR_SECRET_NO_SUCH_OBJECT}; } + } - auto ret = updateItem(); - if (ret.err()) { - m_item->doDelete(); - return ret; + // the item may be locked due to authorization + 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 DBusResult{QDBusError::InternalError}; } - emit completed(false, QVariant::fromValue(m_item->objectPath())); - } else { - bool locked = false; - auto ret = m_item->locked(client, locked); + // postpone anything after the confirmation + connect(prompt, &PromptBase::completed, this, [this]() { + auto res = updateItem(); + finishPrompt(res.err()); + }); + + ret = prompt->prompt(client, windowId); 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 PromptResult::Pending; } - 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))); + // the item can be updated directly + return updateItem(); } DBusResult CreateItemPrompt::updateItem() diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h index e3de1916a..2e9ffed9c 100644 --- a/src/fdosecrets/objects/Prompt.h +++ b/src/fdosecrets/objects/Prompt.h @@ -32,14 +32,60 @@ namespace FdoSecrets class Service; + // a simple helper class to auto convert + // true/false, DBusResult and Pending values + class PromptResult + { + enum Value + { + Accepted, + Dismissed, + AsyncPending, + }; + const Value value; + + explicit PromptResult(Value v) noexcept + : value(v) + { + } + explicit PromptResult(bool accepted) + : value(accepted ? Accepted : Dismissed) + { + } + + public: + PromptResult() + : PromptResult(true) + { + } + PromptResult(const DBusResult& res) // NOLINT(google-explicit-constructor) + : PromptResult(res.ok()) + { + } + + static const PromptResult Pending; + static PromptResult accepted(bool accepted) + { + return PromptResult{accepted}; + } + + bool isDismiss() const + { + return value == Dismissed; + } + bool isPending() const + { + return value == AsyncPending; + } + }; + class PromptBase : public DBusObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT_LITERAL) public: - Q_INVOKABLE virtual DBusResult prompt(const DBusClientPtr& client, const QString& windowId) = 0; - - Q_INVOKABLE virtual DBusResult dismiss(); + Q_INVOKABLE DBusResult prompt(const DBusClientPtr& client, const QString& windowId); + Q_INVOKABLE DBusResult dismiss(); template static PromptBase* Create(Service* parent, ARGS&&... args) { @@ -57,8 +103,15 @@ namespace FdoSecrets protected: explicit PromptBase(Service* parent); + virtual PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) = 0; + virtual QVariant currentResult() const; + QWindow* findWindow(const QString& windowId); Service* service() const; + void finishPrompt(bool dismissed); + + private: + bool m_signalSent = false; }; class Collection; @@ -66,14 +119,11 @@ namespace FdoSecrets class DeleteCollectionPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit DeleteCollectionPrompt(Service* parent, Collection* coll); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - - private: - friend class PromptBase; + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; QPointer m_collection; }; @@ -81,32 +131,27 @@ namespace FdoSecrets class CreateCollectionPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - DBusResult dismiss() override; - - private: - friend class PromptBase; + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; + QVariant currentResult() const override; QVariantMap m_properties; QString m_alias; + Collection* m_coll{}; }; class LockCollectionsPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit LockCollectionsPrompt(Service* parent, const QList& colls); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - DBusResult dismiss() override; - - private: - friend class PromptBase; + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; + QVariant currentResult() const override; QList> m_collections; QList m_locked; @@ -116,22 +161,17 @@ namespace FdoSecrets class UnlockPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit UnlockPrompt(Service* parent, const QSet& colls, const QSet& items); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - DBusResult dismiss() override; + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; + QVariant currentResult() const 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; @@ -148,14 +188,11 @@ namespace FdoSecrets class DeleteItemPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit DeleteItemPrompt(Service* parent, Item* item); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - - private: - friend class PromptBase; + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; QPointer m_item; }; @@ -163,29 +200,24 @@ namespace FdoSecrets class CreateItemPrompt : public PromptBase { Q_OBJECT + friend class PromptBase; explicit CreateItemPrompt(Service* parent, Collection* coll, QVariantMap properties, Secret secret, - QString itemPath, - Item* existing); + bool replace); - public: - DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; - DBusResult dismiss() override; - private slots: - void itemUnlocked(bool dismissed, const QVariant& result); + PromptResult promptSync(const DBusClientPtr& client, const QString& windowId) override; + QVariant currentResult() const override; - private: DBusResult updateItem(); - friend class PromptBase; - QPointer m_coll; QVariantMap m_properties; Secret m_secret; - QString m_itemPath; + bool m_replace; + QPointer m_item; QPointer m_sess; diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index dc42b632e..60e057e7d 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -52,8 +52,10 @@ namespace FdoSecrets , m_databases(std::move(dbTabs)) , m_insideEnsureDefaultAlias(false) { - connect( - m_databases, &DatabaseTabWidget::databaseUnlockDialogFinished, this, &Service::doneUnlockDatabaseInDialog); + connect(m_databases, + &DatabaseTabWidget::databaseUnlockDialogFinished, + this, + &Service::onDatabaseUnlockDialogFinished); } Service::~Service() = default; @@ -86,7 +88,7 @@ namespace FdoSecrets { // The Collection will monitor the database's exposed group. // When the Collection finds that no exposed group, it will delete itself. - // Thus the service also needs to monitor it and recreate the collection if the user changes + // Thus, the service also needs to monitor it and recreate the collection if the user changes // from no exposed to exposed something. connect(dbWidget, &DatabaseWidget::databaseReplaced, this, [this, dbWidget]() { monitorDatabaseExposedGroup(dbWidget); @@ -119,16 +121,16 @@ namespace FdoSecrets // m_backend may already be reset to nullptr // We want to remove the collection object from dbus as early as possible, to avoid // race conditions when deleteLater was called on the m_backend, but not delivered yet, - // and new method calls from dbus occurred. Therefore we can't rely on the destroyed + // and new method calls from dbus occurred. Therefore, we can't rely on the destroyed // signal on m_backend. // bind to coll lifespan connect(m_databases.data(), &DatabaseTabWidget::databaseClosed, coll, [coll](const QString& filePath) { if (filePath == coll->backendFilePath()) { - coll->doDelete(); + coll->removeFromDBus(); } }); - // actual load, must after updates to m_collections, because the reload may trigger + // actual load, must after updates to m_collections, because reloading may trigger // another onDatabaseTabOpen, and m_collections will be used to prevent recursion. if (!coll->reloadBackend()) { // error in dbus @@ -250,7 +252,7 @@ namespace FdoSecrets for (const auto& coll : asConst(colls)) { QList items; - ret = coll->searchItems(attributes, items); + ret = coll->searchItems(client, attributes, items); if (ret.err()) { return ret; } @@ -282,7 +284,7 @@ namespace FdoSecrets itemsToUnlock.reserve(objects.size()); for (const auto& obj : asConst(objects)) { - // the object is either an item or an collection + // the object is either an item or a collection auto item = qobject_cast(obj); auto coll = item ? item->collection() : qobject_cast(obj); // either way there should be a collection @@ -317,6 +319,7 @@ namespace FdoSecrets } } + prompt = nullptr; if (!collectionsToUnlock.isEmpty() || !itemsToUnlock.isEmpty()) { prompt = PromptBase::Create(this, collectionsToUnlock, itemsToUnlock); if (!prompt) { @@ -493,9 +496,44 @@ namespace FdoSecrets m_plugin->emitRequestSwitchToDatabases(); } + bool Service::doLockDatabase(DatabaseWidget* dbWidget) + { + // return immediately if the db is already unlocked + if (dbWidget && dbWidget->isLocked()) { + return true; + } + + // mark the db as being unlocked to prevent multiple dialogs for the same db + if (m_lockingDb.contains(dbWidget)) { + return true; + } + m_lockingDb.insert(dbWidget); + auto ret = dbWidget->lock(); + m_lockingDb.remove(dbWidget); + + return ret; + } + void Service::doUnlockDatabaseInDialog(DatabaseWidget* dbWidget) { + // return immediately if the db is already unlocked + if (dbWidget && !dbWidget->isLocked()) { + emit doneUnlockDatabaseInDialog(true, dbWidget); + return; + } + + // mark the db as being unlocked to prevent multiple dialogs for the same db + if (m_unlockingDb.contains(dbWidget)) { + return; + } + m_unlockingDb.insert(dbWidget); + m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None); } + void Service::onDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget) + { + m_unlockingDb.remove(dbWidget); + emit doneUnlockDatabaseInDialog(accepted, dbWidget); + } } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Service.h b/src/fdosecrets/objects/Service.h index 84eee230d..422a3ca94 100644 --- a/src/fdosecrets/objects/Service.h +++ b/src/fdosecrets/objects/Service.h @@ -96,8 +96,8 @@ namespace FdoSecrets /** * Finish signal for async action doUnlockDatabaseInDialog - * @param accepted If false, the action is canceled by the user - * @param dbWidget The unlocked the dbWidget if succeed + * @param accepted If false, the action is cancelled by the user + * @param dbWidget The dbWidget the action is on */ void doneUnlockDatabaseInDialog(bool accepted, DatabaseWidget* dbWidget); @@ -114,6 +114,7 @@ namespace FdoSecrets } public slots: + bool doLockDatabase(DatabaseWidget* dbWidget); bool doCloseDatabase(DatabaseWidget* dbWidget); Collection* doNewDatabase(); void doSwitchToDatabaseSettings(DatabaseWidget* dbWidget); @@ -135,6 +136,8 @@ namespace FdoSecrets void onCollectionAliasRemoved(const QString& alias); + void onDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget); + private: bool initialize(); @@ -153,16 +156,18 @@ namespace FdoSecrets Collection* findCollection(const DatabaseWidget* db) const; private: - FdoSecretsPlugin* m_plugin; - QPointer m_databases; + FdoSecretsPlugin* m_plugin{nullptr}; + QPointer m_databases{}; - QHash m_aliases; - QList m_collections; - QHash m_dbToCollection; + QHash m_aliases{}; + QList m_collections{}; + QHash m_dbToCollection{}; - QList m_sessions; + QList m_sessions{}; - bool m_insideEnsureDefaultAlias; + bool m_insideEnsureDefaultAlias{false}; + QSet m_unlockingDb{}; // list of db being unlocking + QSet m_lockingDb{}; // list of db being locking }; } // namespace FdoSecrets diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 5ab22749b..842a40b98 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -255,6 +255,7 @@ void SettingsWidgetFdoSecrets::loadSettings() m_ui->showNotification->setChecked(FdoSecrets::settings()->showNotification()); m_ui->confirmDeleteItem->setChecked(FdoSecrets::settings()->confirmDeleteItem()); m_ui->confirmAccessItem->setChecked(FdoSecrets::settings()->confirmAccessItem()); + m_ui->unlockBeforeSearch->setChecked(FdoSecrets::settings()->unlockBeforeSearch()); } void SettingsWidgetFdoSecrets::saveSettings() @@ -263,6 +264,7 @@ void SettingsWidgetFdoSecrets::saveSettings() FdoSecrets::settings()->setShowNotification(m_ui->showNotification->isChecked()); FdoSecrets::settings()->setConfirmDeleteItem(m_ui->confirmDeleteItem->isChecked()); FdoSecrets::settings()->setConfirmAccessItem(m_ui->confirmAccessItem->isChecked()); + FdoSecrets::settings()->setUnlockBeforeSearch(m_ui->unlockBeforeSearch->isChecked()); } void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index 7034d7bd6..bed666676 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -72,7 +72,11 @@ - <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> + <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 @@ -82,6 +86,23 @@ + + + + <html><head/><body><p>This improves compatibility with certain applications + which search for password without unlocking the database first.</p><p>But enabling this may also + crash the client if the database can not be unlocked within a certain timeout. (Usually 25s, but may be a + different value set in applications.)</p></body></html> + + + + Prompt to unlock database before searching + + + true + + + diff --git a/src/gui/GuiTools.cpp b/src/gui/GuiTools.cpp index 7aeeeb7b0..30963b374 100644 --- a/src/gui/GuiTools.cpp +++ b/src/gui/GuiTools.cpp @@ -66,10 +66,10 @@ namespace GuiTools } } - void deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent) + size_t deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent) { if (!parent || entries.isEmpty()) { - return; + return 0; } QList selectedEntries; @@ -116,5 +116,6 @@ namespace GuiTools entry->database()->recycleEntry(entry); } } + return selectedEntries.size(); } } // namespace GuiTools diff --git a/src/gui/GuiTools.h b/src/gui/GuiTools.h index 14a54ab75..814537382 100644 --- a/src/gui/GuiTools.h +++ b/src/gui/GuiTools.h @@ -26,6 +26,6 @@ class Entry; namespace GuiTools { bool confirmDeleteEntries(QWidget* parent, const QList& entries, bool permanent); - void deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent); + size_t deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent); } // namespace GuiTools #endif // KEEPASSXC_GUITOOLS_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index e736efaad..f9527966b 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -69,20 +69,20 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) , m_browserUi(new Ui::EditEntryWidgetBrowser()) , m_attachments(new EntryAttachments()) , m_customData(new CustomData()) - , m_mainWidget(new QScrollArea()) - , m_advancedWidget(new QWidget()) - , m_iconsWidget(new EditWidgetIcons()) - , m_autoTypeWidget(new QWidget()) + , m_mainWidget(new QScrollArea(this)) + , m_advancedWidget(new QWidget(this)) + , m_iconsWidget(new EditWidgetIcons(this)) + , m_autoTypeWidget(new QWidget(this)) #ifdef WITH_XC_SSHAGENT - , m_sshAgentWidget(new QWidget()) + , m_sshAgentWidget(new QWidget(this)) #endif #ifdef WITH_XC_BROWSER , m_browserSettingsChanged(false) - , m_browserWidget(new QWidget()) + , m_browserWidget(new QWidget(this)) , m_additionalURLsDataModel(new EntryURLModel(this)) #endif - , m_editWidgetProperties(new EditWidgetProperties()) - , m_historyWidget(new QWidget()) + , m_editWidgetProperties(new EditWidgetProperties(this)) + , m_historyWidget(new QWidget(this)) , m_entryAttributes(new EntryAttributes(this)) , m_attributesModel(new EntryAttributesModel(m_advancedWidget)) , m_historyModel(new EntryHistoryModel(this)) diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp index 5e8bd01cd..ffdcd338f 100644 --- a/tests/gui/TestGuiFdoSecrets.cpp +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -186,7 +186,7 @@ void TestGuiFdoSecrets::init() // make sure window is activated or focus tests may fail m_mainWindow->activateWindow(); - QApplication::processEvents(); + processEvents(); // open and unlock the database m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a"); @@ -202,6 +202,7 @@ void TestGuiFdoSecrets::init() void TestGuiFdoSecrets::cleanup() { // restore to default settings + FdoSecrets::settings()->setUnlockBeforeSearch(false); FdoSecrets::settings()->setShowNotification(false); FdoSecrets::settings()->setConfirmAccessItem(false); FdoSecrets::settings()->setEnabled(false); @@ -213,8 +214,14 @@ void TestGuiFdoSecrets::cleanup() for (int i = 0; i != m_tabWidget->count(); ++i) { m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean(); } + + // Close any dialogs + while (auto w = QApplication::activeModalWidget()) { + w->close(); + } + VERIFY(m_tabWidget->closeAllDatabaseTabs()); - QApplication::processEvents(); + processEvents(); if (m_dbFile) { m_dbFile->remove(); @@ -250,7 +257,7 @@ void TestGuiFdoSecrets::testServiceEnable() VERIFY(sigError.isEmpty()); COMPARE(sigStarted.size(), 1); - QApplication::processEvents(); + processEvents(); VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); @@ -288,9 +295,11 @@ void TestGuiFdoSecrets::testServiceSearch() auto item = getFirstItem(coll); VERIFY(item); - auto entries = m_db->rootGroup()->entriesRecursive(false); - VERIFY(!entries.isEmpty()); - const auto& entry = entries.first(); + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + entry->attributes()->set("fdosecrets-test", "1"); entry->attributes()->set("fdosecrets-test-protected", "2", true); const QString crazyKey = "_a:bc&-+'-e%12df_d"; @@ -336,6 +345,72 @@ void TestGuiFdoSecrets::testServiceSearch() } } +void TestGuiFdoSecrets::testServiceSearchBlockingUnlock() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + auto entries = m_db->rootGroup()->entriesRecursive(); + VERIFY(!entries.isEmpty()); + // assumes the db is not empty + auto title = entries.first()->title(); + + // NOTE: entries are no longer valid after locking + lockDatabaseInBackend(); + + // when database is locked, nothing is returned + FdoSecrets::settings()->setUnlockBeforeSearch(false); + { + DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); + } + + // when database is locked, nothing is returned + FdoSecrets::settings()->setUnlockBeforeSearch(true); + { + // SearchItems will block because the blocking wait is implemented + // using a local QEventLoop. + // so we do a little trick here to get the return value back + bool unlockDialogWorks = false; + QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(); }); + + DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}})); + VERIFY(unlockDialogWorks); + COMPARE(locked, {}); + COMPARE(unlocked.size(), 1); + auto item = getProxy(unlocked.first()); + DBUS_COMPARE(item->label(), title); + } +} + +void TestGuiFdoSecrets::testServiceSearchForce() +{ + 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); + + // fdosecrets should still find the item even if searching is disabled + entry->group()->setSearchingEnabled(Group::Disable); + + // search by title + { + DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); + } +} + void TestGuiFdoSecrets::testServiceUnlock() { lockDatabaseInBackend(); @@ -362,46 +437,83 @@ void TestGuiFdoSecrets::testServiceUnlock() VERIFY(spyPromptCompleted.isValid()); // nothing is unlocked yet - QTRY_COMPARE(spyPromptCompleted.count(), 0); + VERIFY(waitForSignal(spyPromptCompleted, 0)); DBUS_COMPARE(coll->locked(), true); - // drive the prompt + // show the prompt DBUS_VERIFY(prompt->Prompt("")); // still not unlocked before user action - QTRY_COMPARE(spyPromptCompleted.count(), 0); + VERIFY(waitForSignal(spyPromptCompleted, 0)); DBUS_COMPARE(coll->locked(), true); - // interact with the dialog - QApplication::processEvents(); - { - auto dbOpenDlg = m_tabWidget->findChild(); - VERIFY(dbOpenDlg); - auto editPassword = dbOpenDlg->findChild("editPassword"); - VERIFY(editPassword); - editPassword->setFocus(); - QTest::keyClicks(editPassword, "a"); - QTest::keyClick(editPassword, Qt::Key_Enter); - } - QApplication::processEvents(); + VERIFY(driveUnlockDialog()); - // unlocked - DBUS_COMPARE(coll->locked(), false); - - QTRY_COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); { auto args = spyPromptCompleted.takeFirst(); COMPARE(args.size(), 2); COMPARE(args.at(0).toBool(), false); COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); } - QTRY_COMPARE(spyCollectionCreated.count(), 0); + + // check unlocked *AFTER* the prompt signal + DBUS_COMPARE(coll->locked(), false); + + VERIFY(waitForSignal(spyCollectionCreated, 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); + VERIFY(waitForSignal(spyCollectionDeleted, 0)); +} + +void TestGuiFdoSecrets::testServiceUnlockDatabaseConcurrent() +{ + lockDatabaseInBackend(); + + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())})); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + DBUS_VERIFY(prompt->Prompt("")); + + // while the first prompt is running, another request come in + DBUS_GET2(unlocked2, promptPath2, service->Unlock({QDBusObjectPath(coll->path())})); + auto prompt2 = getProxy(promptPath2); + VERIFY(prompt2); + QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted2.isValid()); + DBUS_VERIFY(prompt2->Prompt("")); + + // there should be only one unlock dialog + VERIFY(driveUnlockDialog()); + + // both prompts should complete + VERIFY(waitForSignal(spyPromptCompleted, 1)); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); + } + VERIFY(waitForSignal(spyPromptCompleted2, 1)); + { + auto args = spyPromptCompleted2.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); + } + + // check unlocked *AFTER* prompt signal + DBUS_COMPARE(coll->locked(), false); } void TestGuiFdoSecrets::testServiceUnlockItems() @@ -438,17 +550,16 @@ void TestGuiFdoSecrets::testServiceUnlockItems() // only allow once VERIFY(driveAccessControlDialog(false)); - // unlocked - DBUS_COMPARE(item->locked(), false); - - VERIFY(spyPromptCompleted.wait()); - COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); { auto args = spyPromptCompleted.takeFirst(); COMPARE(args.size(), 2); COMPARE(args.at(0).toBool(), false); COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); } + + // unlocked + DBUS_COMPARE(item->locked(), false); } // access the secret should reset the locking state @@ -477,17 +588,16 @@ void TestGuiFdoSecrets::testServiceUnlockItems() // only allow and remember VERIFY(driveAccessControlDialog(true)); - // unlocked - DBUS_COMPARE(item->locked(), false); - - VERIFY(spyPromptCompleted.wait()); - COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); { auto args = spyPromptCompleted.takeFirst(); COMPARE(args.size(), 2); COMPARE(args.at(0).toBool(), false); COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); } + + // unlocked + DBUS_COMPARE(item->locked(), false); } // access the secret does not reset the locking state @@ -524,15 +634,15 @@ void TestGuiFdoSecrets::testServiceLock() // prompt and click cancel MessageBox::setNextAnswer(MessageBox::Cancel); DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); + processEvents(); - DBUS_COMPARE(coll->locked(), false); - - QTRY_COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); auto args = spyPromptCompleted.takeFirst(); COMPARE(args.count(), 2); COMPARE(args.at(0).toBool(), true); COMPARE(getSignalVariantArgument>(args.at(1)), {}); + + DBUS_COMPARE(coll->locked(), false); } { DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); @@ -545,24 +655,24 @@ void TestGuiFdoSecrets::testServiceLock() // prompt and click save MessageBox::setNextAnswer(MessageBox::Save); DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); + processEvents(); - DBUS_COMPARE(coll->locked(), true); - - QTRY_COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); auto args = spyPromptCompleted.takeFirst(); COMPARE(args.count(), 2); COMPARE(args.at(0).toBool(), false); COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); + + DBUS_COMPARE(coll->locked(), true); } - QTRY_COMPARE(spyCollectionCreated.count(), 0); + VERIFY(waitForSignal(spyCollectionCreated, 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); + VERIFY(waitForSignal(spyCollectionDeleted, 0)); // locking item locks the whole db unlockDatabaseInBackend(); @@ -575,12 +685,59 @@ void TestGuiFdoSecrets::testServiceLock() MessageBox::setNextAnswer(MessageBox::Save); DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); + processEvents(); DBUS_COMPARE(coll->locked(), true); } } +void TestGuiFdoSecrets::testServiceLockConcurrent() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + m_db->markAsModified(); + + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + DBUS_GET2(locked2, promptPath2, service->Lock({QDBusObjectPath(coll->path())})); + auto prompt2 = getProxy(promptPath2); + VERIFY(prompt2); + QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted2.isValid()); + + // prompt and click save + MessageBox::setNextAnswer(MessageBox::Save); + DBUS_VERIFY(prompt->Prompt("")); + + // second prompt should not show dialog + DBUS_VERIFY(prompt2->Prompt("")); + + VERIFY(waitForSignal(spyPromptCompleted, 1)); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); + } + + VERIFY(waitForSignal(spyPromptCompleted2, 1)); + { + auto args = spyPromptCompleted2.takeFirst(); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); + } + + DBUS_COMPARE(coll->locked(), true); +} + void TestGuiFdoSecrets::testSessionOpen() { auto service = enableService(); @@ -621,7 +778,7 @@ void TestGuiFdoSecrets::testCollectionCreate() COMPARE(promptPath, QDBusObjectPath("/")); COMPARE(collPath.path(), existing->path()); } - QTRY_COMPARE(spyCollectionCreated.count(), 0); + VERIFY(waitForSignal(spyCollectionCreated, 0)); // create new one and set properties { @@ -635,11 +792,10 @@ void TestGuiFdoSecrets::testCollectionCreate() QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); VERIFY(spyPromptCompleted.isValid()); - QTimer::singleShot(50, this, &TestGuiFdoSecrets::driveNewDatabaseWizard); DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); + VERIFY(driveNewDatabaseWizard()); - QTRY_COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); auto args = spyPromptCompleted.takeFirst(); COMPARE(args.size(), 2); COMPARE(args.at(0).toBool(), false); @@ -648,7 +804,7 @@ void TestGuiFdoSecrets::testCollectionCreate() DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB")); - QTRY_COMPARE(spyCollectionCreated.count(), 1); + VERIFY(waitForSignal(spyCollectionCreated, 1)); { args = spyCollectionCreated.takeFirst(); COMPARE(args.size(), 1); @@ -657,34 +813,6 @@ void TestGuiFdoSecrets::testCollectionCreate() } } -void TestGuiFdoSecrets::driveNewDatabaseWizard() -{ - auto wizard = m_tabWidget->findChild(); - VERIFY(wizard); - - COMPARE(wizard->currentId(), 0); - wizard->next(); - wizard->next(); - COMPARE(wizard->currentId(), 2); - - // enter password - auto* passwordEdit = wizard->findChild("enterPasswordEdit"); - auto* passwordRepeatEdit = wizard->findChild("repeatPasswordEdit"); - QTest::keyClicks(passwordEdit, "test"); - QTest::keyClick(passwordEdit, Qt::Key::Key_Tab); - QTest::keyClicks(passwordRepeatEdit, "test"); - - // save database to temporary file - TemporaryFile tmpFile; - VERIFY(tmpFile.open()); - tmpFile.close(); - fileDialog()->setNextFileName(tmpFile.fileName()); - - wizard->accept(); - - tmpFile.remove(); -} - void TestGuiFdoSecrets::testCollectionDelete() { auto service = enableService(); @@ -711,7 +839,12 @@ void TestGuiFdoSecrets::testCollectionDelete() // 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 - QApplication::processEvents(); + + VERIFY(waitForSignal(spyPromptCompleted, 1)); + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); // however, the object should already be taken down from dbus { @@ -720,13 +853,7 @@ void TestGuiFdoSecrets::testCollectionDelete() COMPARE(reply.error().type(), QDBusError::UnknownObject); } - QTRY_COMPARE(spyPromptCompleted.count(), 1); - auto args = spyPromptCompleted.takeFirst(); - COMPARE(args.count(), 2); - COMPARE(args.at(0).toBool(), false); - COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); - - QTRY_COMPARE(spyCollectionDeleted.count(), 1); + VERIFY(waitForSignal(spyCollectionDeleted, 1)); { args = spyCollectionDeleted.takeFirst(); COMPARE(args.size(), 1); @@ -734,6 +861,57 @@ void TestGuiFdoSecrets::testCollectionDelete() } } +void TestGuiFdoSecrets::testCollectionDeleteConcurrent() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + m_db->markAsModified(); + DBUS_GET(promptPath, coll->Delete()); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // before interacting with the prompt, another request come in + DBUS_GET(promptPath2, coll->Delete()); + auto prompt2 = getProxy(promptPath); + VERIFY(prompt2); + QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted2.isValid()); + + // prompt and click save + MessageBox::setNextAnswer(MessageBox::Save); + DBUS_VERIFY(prompt->Prompt("")); + + // there should be no prompt + DBUS_VERIFY(prompt2->Prompt("")); + + VERIFY(waitForSignal(spyPromptCompleted, 1)); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); + } + + VERIFY(waitForSignal(spyPromptCompleted2, 1)); + { + auto args = spyPromptCompleted2.takeFirst(); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); + } + + { + auto reply = coll->locked(); + VERIFY(reply.isFinished() && reply.isError()); + COMPARE(reply.error().type(), QDBusError::UnknownObject); + } +} + void TestGuiFdoSecrets::testCollectionChange() { auto service = enableService(); @@ -822,7 +1000,7 @@ void TestGuiFdoSecrets::testItemCreate() // signals { - QTRY_COMPARE(spyItemCreated.count(), 1); + VERIFY(waitForSignal(spyItemCreated, 1)); auto args = spyItemCreated.takeFirst(); COMPARE(args.size(), 1); COMPARE(args.at(0).value().path(), item->path()); @@ -942,7 +1120,7 @@ void TestGuiFdoSecrets::testItemReplace() QSet expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())}; COMPARE(QSet::fromList(unlocked), expected); - QTRY_COMPARE(spyItemCreated.count(), 0); + VERIFY(waitForSignal(spyItemCreated, 0)); // there may be multiple changed signals, due to each item attribute is set separately QTRY_VERIFY(!spyItemChanged.isEmpty()); for (const auto& args : spyItemChanged) { @@ -968,7 +1146,7 @@ void TestGuiFdoSecrets::testItemReplace() }; COMPARE(QSet::fromList(unlocked), expected); - QTRY_COMPARE(spyItemCreated.count(), 1); + VERIFY(waitForSignal(spyItemCreated, 1)); { auto args = spyItemCreated.takeFirst(); COMPARE(args.size(), 1); @@ -1013,7 +1191,7 @@ void TestGuiFdoSecrets::testItemReplaceExistingLocked() DBUS_COMPARE(item->locked(), true); } - // when replace with a locked item, there will be an prompt + // when replace with a locked item, there will be a prompt auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true); VERIFY(item2); COMPARE(item2->path(), item->path()); @@ -1061,7 +1239,7 @@ void TestGuiFdoSecrets::testItemSecret() COMPARE(ss.contentType, TEXT_PLAIN); COMPARE(ss.value, entry->password().toUtf8()); - QTRY_COMPARE(spyShowNotification.count(), 1); + VERIFY(waitForSignal(spyShowNotification, 1)); } FdoSecrets::settings()->setShowNotification(false); @@ -1125,15 +1303,14 @@ void TestGuiFdoSecrets::testItemDelete() VERIFY(itemObj); MessageBox::setNextAnswer(MessageBox::Delete); DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); - QTRY_COMPARE(spyPromptCompleted.count(), 1); + VERIFY(waitForSignal(spyPromptCompleted, 1)); auto args = spyPromptCompleted.takeFirst(); COMPARE(args.count(), 2); COMPARE(args.at(0).toBool(), false); COMPARE(args.at(1).toString(), QStringLiteral("")); - QTRY_COMPARE(spyItemDeleted.count(), 1); + VERIFY(waitForSignal(spyItemDeleted, 1)); args = spyItemDeleted.takeFirst(); COMPARE(args.size(), 1); COMPARE(args.at(0).value().path(), itemPath); @@ -1275,7 +1452,7 @@ void TestGuiFdoSecrets::testModifyingExposedGroup() m_db->metadata()->setRecycleBinEnabled(true); m_db->recycleGroup(subgroup); - QApplication::processEvents(); + processEvents(); { DBUS_GET(collPaths, service->collections()); @@ -1284,7 +1461,7 @@ void TestGuiFdoSecrets::testModifyingExposedGroup() // test setting another exposed group, the collection will be exposed again FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); - QApplication::processEvents(); + processEvents(); { DBUS_GET(collPaths, service->collections()); COMPARE(collPaths.size(), 1); @@ -1295,14 +1472,25 @@ void TestGuiFdoSecrets::lockDatabaseInBackend() { m_dbWidget->lock(); m_db.reset(); - QApplication::processEvents(); + processEvents(); } void TestGuiFdoSecrets::unlockDatabaseInBackend() { m_dbWidget->performUnlockDatabase("a"); m_db = m_dbWidget->database(); - QApplication::processEvents(); + processEvents(); +} + +void TestGuiFdoSecrets::processEvents() +{ + // Couldn't use QApplication::processEvents, because per Qt documentation: + // events that are posted while the function runs will be queued until a later round of event processing. + // and we may post QTimer single shot events during event handling to achieve async method. + // So we directly call event dispatcher in a loop until no events can be handled + while (QAbstractEventDispatcher::instance()->processEvents(QEventLoop::AllEvents)) { + // pass + } } // the following functions have return value, switch macros to the version supporting that @@ -1388,9 +1576,7 @@ QSharedPointer TestGuiFdoSecrets::createItem(const QSharedPointer TestGuiFdoSecrets::createItem(const QSharedPointerallWidgets()) { + processEvents(); + for (auto w : QApplication::topLevelWidgets()) { if (!w->isWindow()) { continue; } auto dlg = qobject_cast(w); - if (dlg) { + if (dlg && dlg->isVisible()) { auto rememberCheck = dlg->findChild("rememberCheck"); VERIFY(rememberCheck); rememberCheck->setChecked(remember); - QTest::keyClick(dlg, Qt::Key_Enter); - QApplication::processEvents(); + dlg->done(AccessControlDialog::AllowSelected); + processEvents(); + VERIFY(dlg->isHidden()); return true; } } return false; } +bool TestGuiFdoSecrets::driveNewDatabaseWizard() +{ + // processEvents will block because the NewDatabaseWizard is shown using exec + // which creates a local QEventLoop. + // so we do a little trick here to get the return value back + bool ret = false; + QTimer::singleShot(0, this, [this, &ret]() { + ret = [this]() -> bool { + auto wizard = m_tabWidget->findChild(); + VERIFY(wizard); + + COMPARE(wizard->currentId(), 0); + wizard->next(); + wizard->next(); + COMPARE(wizard->currentId(), 2); + + // enter password + auto* passwordEdit = wizard->findChild("enterPasswordEdit"); + auto* passwordRepeatEdit = wizard->findChild("repeatPasswordEdit"); + VERIFY(passwordEdit); + VERIFY(passwordRepeatEdit); + QTest::keyClicks(passwordEdit, "test"); + QTest::keyClick(passwordEdit, Qt::Key::Key_Tab); + QTest::keyClicks(passwordRepeatEdit, "test"); + + // save database to temporary file + TemporaryFile tmpFile; + VERIFY(tmpFile.open()); + tmpFile.close(); + fileDialog()->setNextFileName(tmpFile.fileName()); + + wizard->accept(); + + tmpFile.remove(); + return true; + }(); + }); + processEvents(); + return ret; +} + +bool TestGuiFdoSecrets::driveUnlockDialog() +{ + processEvents(); + auto dbOpenDlg = m_tabWidget->findChild(); + VERIFY(dbOpenDlg); + auto editPassword = dbOpenDlg->findChild("editPassword"); + VERIFY(editPassword); + editPassword->setFocus(); + QTest::keyClicks(editPassword, "a"); + QTest::keyClick(editPassword, Qt::Key_Enter); + processEvents(); + return true; +} + +bool TestGuiFdoSecrets::waitForSignal(QSignalSpy& spy, int expectedCount) +{ + processEvents(); + // If already expected count, do not wait and return immediately + if (spy.count() == expectedCount) { + return true; + } else if (spy.count() > expectedCount) { + return false; + } + spy.wait(); + COMPARE(spy.count(), expectedCount); + return true; +} + #undef VERIFY #define VERIFY QVERIFY #undef COMPARE diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h index a53da4088..1ed6d7f66 100644 --- a/tests/gui/TestGuiFdoSecrets.h +++ b/tests/gui/TestGuiFdoSecrets.h @@ -23,6 +23,7 @@ #include #include "fdosecrets/dbus/DBusTypes.h" +#include "gui/MessageBox.h" class MainWindow; class Database; @@ -47,6 +48,7 @@ class SessionProxy; class PromptProxy; class QAbstractItemView; +class QSignalSpy; class TestGuiFdoSecrets : public QObject { @@ -64,15 +66,20 @@ private slots: void testServiceEnable(); void testServiceEnableNoExposedDatabase(); void testServiceSearch(); + void testServiceSearchBlockingUnlock(); + void testServiceSearchForce(); void testServiceUnlock(); + void testServiceUnlockDatabaseConcurrent(); void testServiceUnlockItems(); void testServiceLock(); + void testServiceLockConcurrent(); void testSessionOpen(); void testSessionClose(); void testCollectionCreate(); void testCollectionDelete(); + void testCollectionDeleteConcurrent(); void testCollectionChange(); void testItemCreate(); @@ -92,11 +99,14 @@ private slots: void testHiddenFilename(); void testDuplicateName(); -protected slots: - void driveNewDatabaseWizard(); - bool driveAccessControlDialog(bool remember = true); - private: + bool driveUnlockDialog(); + bool driveNewDatabaseWizard(); + bool driveAccessControlDialog(bool remember = true); + bool waitForSignal(QSignalSpy& spy, int expectedCount); + + void processEvents(); + void lockDatabaseInBackend(); void unlockDatabaseInBackend(); QSharedPointer enableService();