From 7d3c3b09fb158adbbc9078a374705bff448f944f Mon Sep 17 00:00:00 2001 From: Aetf Date: Sat, 26 Feb 2022 17:22:20 -0500 Subject: [PATCH] FdoSecrest: allow remember decision for future entries Also added a reset decision button in session management tab Fixes #7464 * Fix distorted button in settings page: the default margin in QToolBar is too large for our use case in a table row. --- share/translations/keepassxc_en.ts | 22 ++++- src/fdosecrets/dbus/DBusClient.cpp | 25 +++-- src/fdosecrets/dbus/DBusClient.h | 4 +- src/fdosecrets/objects/Prompt.cpp | 9 +- src/fdosecrets/objects/Prompt.h | 2 +- .../widgets/AccessControlDialog.cpp | 68 +++++++------ src/fdosecrets/widgets/AccessControlDialog.h | 4 +- src/fdosecrets/widgets/AccessControlDialog.ui | 10 -- .../widgets/SettingsWidgetFdoSecrets.cpp | 97 +++++++++++-------- tests/gui/TestGuiFdoSecrets.cpp | 66 ++++++++++++- tests/gui/TestGuiFdoSecrets.h | 3 +- 11 files changed, 206 insertions(+), 104 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 0f706445f..2b83e1d2c 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -78,10 +78,6 @@ Command Line - - Your decision for above entries will be remembered for the duration the requesting client is running. - - Details @@ -95,7 +91,15 @@ - Deny All + Your decision will be remembered for the duration while both the requesting client AND KeePassXC are running. + + + + Deny All && Future + + + + Allow All && &Future @@ -5398,6 +5402,14 @@ We recommend you use the AppImage available on our downloads page. Disconnect this application + + Reset + + + + Reset any remembered decisions for this application + + Merger diff --git a/src/fdosecrets/dbus/DBusClient.cpp b/src/fdosecrets/dbus/DBusClient.cpp index 6e0f63486..36cb927bf 100644 --- a/src/fdosecrets/dbus/DBusClient.cpp +++ b/src/fdosecrets/dbus/DBusClient.cpp @@ -73,8 +73,8 @@ namespace FdoSecrets 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); + return m_authorizedAll != AuthDecision::Undecided || m_allowed.contains(uuid) || m_allowedOnce.contains(uuid) + || m_denied.contains(uuid) || m_deniedOnce.contains(uuid); } bool DBusClient::itemAuthorized(const QUuid& uuid) const @@ -83,10 +83,16 @@ namespace FdoSecrets // everyone is authorized if this is not enabled return true; } - if (m_authorizedAll) { - // this client is trusted + + // check if we have catch-all decision + if (m_authorizedAll == AuthDecision::Allowed) { return true; } + if (m_authorizedAll == AuthDecision::Denied) { + return false; + } + + // individual decisions if (m_deniedOnce.contains(uuid) || m_denied.contains(uuid)) { // explicitly denied return false; @@ -132,14 +138,21 @@ namespace FdoSecrets } } - void DBusClient::setAllAuthorized(bool authorized) + void DBusClient::setAllAuthorized(AuthDecision authorized) { + // once variants doesn't make sense here + if (authorized == AuthDecision::AllowedOnce) { + authorized = AuthDecision::Allowed; + } + if (authorized == AuthDecision::DeniedOnce) { + authorized = AuthDecision::Denied; + } m_authorizedAll = authorized; } void DBusClient::clearAuthorization() { - m_authorizedAll = false; + m_authorizedAll = AuthDecision::Undecided; m_allowed.clear(); m_allowedOnce.clear(); m_denied.clear(); diff --git a/src/fdosecrets/dbus/DBusClient.h b/src/fdosecrets/dbus/DBusClient.h index 4fc333e1c..3d9e94d05 100644 --- a/src/fdosecrets/dbus/DBusClient.h +++ b/src/fdosecrets/dbus/DBusClient.h @@ -159,7 +159,7 @@ namespace FdoSecrets /** * Authorize client to access all items. */ - void setAllAuthorized(bool authorized = true); + void setAllAuthorized(AuthDecision authorized); /** * Forget all previous authorization. @@ -176,7 +176,7 @@ namespace FdoSecrets QPointer m_dbus; PeerInfo m_process; - bool m_authorizedAll{false}; + AuthDecision m_authorizedAll{AuthDecision::Undecided}; QSet m_allowed{}; QSet m_denied{}; diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp index 58fdfa4e1..aa0de5404 100644 --- a/src/fdosecrets/objects/Prompt.cpp +++ b/src/fdosecrets/objects/Prompt.cpp @@ -303,7 +303,7 @@ namespace FdoSecrets } continue; } - // attach a temporary property so later we can get the item + // 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; @@ -317,11 +317,11 @@ namespace FdoSecrets connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater); ac->open(); } else { - itemUnlockFinished({}); + itemUnlockFinished({}, AuthDecision::Undecided); } } - void UnlockPrompt::itemUnlockFinished(const QHash& decisions) + void UnlockPrompt::itemUnlockFinished(const QHash& decisions, AuthDecision forFutureEntries) { auto client = m_client.lock(); if (!client) { @@ -345,6 +345,9 @@ namespace FdoSecrets m_numRejected += 1; } } + if (forFutureEntries != AuthDecision::Undecided) { + client->setAllAuthorized(forFutureEntries); + } // if anything is not unlocked, treat the whole prompt as dismissed // so the client has a chance to handle the error finishPrompt(m_numRejected > 0); diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h index 2e9ffed9c..599343564 100644 --- a/src/fdosecrets/objects/Prompt.h +++ b/src/fdosecrets/objects/Prompt.h @@ -169,7 +169,7 @@ namespace FdoSecrets QVariant currentResult() const override; void collectionUnlockFinished(bool accepted); - void itemUnlockFinished(const QHash& results); + void itemUnlockFinished(const QHash& results, AuthDecision forFutureEntries); void unlockItems(); static constexpr auto FdoSecretsBackend = "FdoSecretsBackend"; diff --git a/src/fdosecrets/widgets/AccessControlDialog.cpp b/src/fdosecrets/widgets/AccessControlDialog.cpp index 2c8fd696a..8e3a3e1d8 100644 --- a/src/fdosecrets/widgets/AccessControlDialog.cpp +++ b/src/fdosecrets/widgets/AccessControlDialog.cpp @@ -68,8 +68,6 @@ AccessControlDialog::AccessControlDialog(QWindow* parent, m_ui->itemsTable->resizeColumnsToContents(); // the info widget - m_ui->rememberMsg->setMessageType(MessageWidget::Information); - m_ui->rememberMsg->show(); // sync with m_rememberCheck->setChecked(true) m_ui->exePathWarn->setMessageType(MessageWidget::Warning); m_ui->exePathWarn->hide(); @@ -81,19 +79,28 @@ AccessControlDialog::AccessControlDialog(QWindow* parent, m_ui->buttonBox->addButton(detailsButtonText + QStringLiteral(" >>"), QDialogButtonBox::HelpRole); detailsButton->setCheckable(true); + QString tooltip = QStringLiteral("

%1

") + .arg(tr("Your decision will be remembered for the duration while both the requesting client " + "AND KeePassXC are running.")); + m_rememberCheck = new QCheckBox(tr("Remember"), this); m_rememberCheck->setObjectName("rememberCheck"); // for testing m_rememberCheck->setChecked(true); m_ui->buttonBox->addButton(m_rememberCheck, QDialogButtonBox::ActionRole); + m_rememberCheck->setToolTip(tooltip); auto allowButton = m_ui->buttonBox->addButton(tr("Allow Selected"), QDialogButtonBox::AcceptRole); allowButton->setDefault(true); - auto cancelButton = m_ui->buttonBox->addButton(tr("Deny All"), QDialogButtonBox::RejectRole); + auto cancelButton = m_ui->buttonBox->addButton(tr("Deny All && Future"), QDialogButtonBox::RejectRole); + cancelButton->setToolTip(tooltip); + + auto allowAllButton = m_ui->buttonBox->addButton(tr("Allow All && &Future"), QDialogButtonBox::AcceptRole); + allowAllButton->setToolTip(tooltip); connect(cancelButton, &QPushButton::clicked, this, [this]() { done(DenyAll); }); connect(allowButton, &QPushButton::clicked, this, [this]() { done(AllowSelected); }); - connect(m_rememberCheck, &QCheckBox::clicked, this, &AccessControlDialog::rememberChecked); + connect(allowAllButton, &QPushButton::clicked, this, [this]() { done(AllowAll); }); connect(detailsButton, &QPushButton::clicked, this, [=](bool checked) { m_ui->detailsContainer->setVisible(checked); if (checked) { @@ -119,6 +126,9 @@ AccessControlDialog::AccessControlDialog(QWindow* parent, detailsButton->click(); } + // adjust size after we initialize the button box + adjustSize(); + allowButton->setFocus(); } @@ -146,15 +156,6 @@ void AccessControlDialog::setupDetails(const FdoSecrets::PeerInfo& info) m_ui->detailsContainer->hide(); } -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); @@ -166,32 +167,35 @@ void AccessControlDialog::denyEntryClicked(Entry* entry, const QModelIndex& inde void AccessControlDialog::dialogFinished(int result) { - auto allow = m_rememberCheck->isChecked() ? AuthDecision::Allowed : AuthDecision::AllowedOnce; - auto deny = m_rememberCheck->isChecked() ? AuthDecision::Denied : AuthDecision::DeniedOnce; + auto decision = AuthDecision::Undecided; + auto futureDecision = AuthDecision::Undecided; + switch (result) { + case AllowSelected: + decision = m_rememberCheck->isChecked() ? AuthDecision::Allowed : AuthDecision::AllowedOnce; + break; + case AllowAll: + decision = AuthDecision::Allowed; + futureDecision = AuthDecision::Allowed; + break; + case DenyAll: + decision = AuthDecision::Denied; + futureDecision = AuthDecision::Denied; + break; + case Rejected: + default: + break; + } 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; - } + + auto undecided = result == AllowSelected && !selected; + m_decisions.insert(entry, undecided ? AuthDecision::Undecided : decision); } - emit finished(m_decisions); + emit finished(m_decisions, futureDecision); } QHash AccessControlDialog::decisions() const diff --git a/src/fdosecrets/widgets/AccessControlDialog.h b/src/fdosecrets/widgets/AccessControlDialog.h index 7f491f7ce..370085007 100644 --- a/src/fdosecrets/widgets/AccessControlDialog.h +++ b/src/fdosecrets/widgets/AccessControlDialog.h @@ -66,16 +66,16 @@ public: { Rejected, AllowSelected, + AllowAll, DenyAll, }; QHash decisions() const; signals: - void finished(const QHash& results); + void finished(const QHash& results, AuthDecision forFutureEntries); private slots: - void rememberChecked(bool checked); void denyEntryClicked(Entry* entry, const QModelIndex& index); void dialogFinished(int result); diff --git a/src/fdosecrets/widgets/AccessControlDialog.ui b/src/fdosecrets/widgets/AccessControlDialog.ui index 3e4785273..faa35ed34 100644 --- a/src/fdosecrets/widgets/AccessControlDialog.ui +++ b/src/fdosecrets/widgets/AccessControlDialog.ui @@ -122,16 +122,6 @@ - - - - Your decision for above entries will be remembered for the duration the requesting client is running. - - - false - - - diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 842a40b98..692a7561b 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -26,7 +26,8 @@ #include "gui/DatabaseWidget.h" -#include +#include +#include using FdoSecrets::DBusClientPtr; using FdoSecrets::SettingsClientModel; @@ -34,7 +35,7 @@ using FdoSecrets::SettingsDatabaseModel; namespace { - class ManageDatabase : public QToolBar + class ManageDatabase : public QWidget { Q_OBJECT @@ -42,18 +43,9 @@ namespace public: explicit ManageDatabase(FdoSecretsPlugin* plugin, QWidget* parent = nullptr) - : QToolBar(parent) + : QWidget(parent) , m_plugin(plugin) { - setFloatable(false); - setMovable(false); - - // use a dummy widget to center the buttons - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - spacer->setVisible(true); - addWidget(spacer); - // db settings m_dbSettingsAct = new QAction(tr("Database settings"), this); m_dbSettingsAct->setIcon(icons()->icon(QStringLiteral("document-edit"))); @@ -63,14 +55,12 @@ namespace if (!m_dbWidget) { return; } - auto db = m_dbWidget; m_plugin->serviceInstance()->doSwitchToDatabaseSettings(m_dbWidget); }); - addAction(m_dbSettingsAct); // unlock/lock m_lockAct = new QAction(tr("Unlock database"), this); - m_lockAct->setIcon(icons()->icon(QStringLiteral("object-locked"))); + m_lockAct->setIcon(icons()->icon(QStringLiteral("object-unlocked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); connect(m_lockAct, &QAction::triggered, this, [this]() { if (!m_dbWidget) { @@ -83,13 +73,23 @@ namespace } }); - addAction(m_lockAct); + // layout + auto dbSettingsBtn = new QToolButton(this); + dbSettingsBtn->setAutoRaise(true); + dbSettingsBtn->setDefaultAction(m_dbSettingsAct); - // use a dummy widget to center the buttons - spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - spacer->setVisible(true); - addWidget(spacer); + auto lockBtn = new QToolButton(this); + lockBtn->setAutoRaise(true); + lockBtn->setDefaultAction(m_lockAct); + + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(1, 1, 1, 1); + layout->setSpacing(1); + + layout->addStretch(); + layout->addWidget(dbSettingsBtn); + layout->addWidget(lockBtn); + layout->addStretch(); } DatabaseWidget* dbWidget() const @@ -127,14 +127,20 @@ namespace return; } connect(m_dbWidget, &DatabaseWidget::databaseLocked, this, [this]() { + if (!m_lockAct || !m_dbSettingsAct) { + return; + } m_lockAct->setText(tr("Unlock database")); - m_lockAct->setIcon(icons()->icon(QStringLiteral("object-locked"))); + m_lockAct->setIcon(icons()->icon(QStringLiteral("object-unlocked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); m_dbSettingsAct->setEnabled(false); }); connect(m_dbWidget, &DatabaseWidget::databaseUnlocked, this, [this]() { + if (!m_lockAct || !m_dbSettingsAct) { + return; + } m_lockAct->setText(tr("Lock database")); - m_lockAct->setIcon(icons()->icon(QStringLiteral("object-unlocked"))); + m_lockAct->setIcon(icons()->icon(QStringLiteral("object-locked"))); m_lockAct->setToolTip(tr("Lock database")); m_dbSettingsAct->setEnabled(true); }); @@ -147,7 +153,7 @@ namespace QAction* m_lockAct = nullptr; }; - class ManageSession : public QToolBar + class ManageSession : public QWidget { Q_OBJECT @@ -155,17 +161,8 @@ namespace public: explicit ManageSession(QWidget* parent = nullptr) - : QToolBar(parent) + : QWidget(parent) { - setFloatable(false); - setMovable(false); - - // use a dummy widget to center the buttons - auto spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - spacer->setVisible(true); - addWidget(spacer); - auto disconnectAct = new QAction(tr("Disconnect"), this); disconnectAct->setIcon(icons()->icon(QStringLiteral("dialog-close"))); disconnectAct->setToolTip(tr("Disconnect this application")); @@ -174,13 +171,33 @@ namespace m_client->disconnectDBus(); } }); - addAction(disconnectAct); - // use a dummy widget to center the buttons - spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - spacer->setVisible(true); - addWidget(spacer); + auto resetAct = new QAction(tr("Reset"), this); + resetAct->setIcon(icons()->icon(QStringLiteral("refresh"))); + resetAct->setToolTip(tr("Reset any remembered decisions for this application")); + connect(resetAct, &QAction::triggered, this, [this]() { + if (m_client) { + m_client->clearAuthorization(); + } + }); + + // layout + auto disconnectBtn = new QToolButton(this); + disconnectBtn->setAutoRaise(true); + disconnectBtn->setDefaultAction(disconnectAct); + + auto resetBtn = new QToolButton(this); + resetBtn->setAutoRaise(true); + resetBtn->setDefaultAction(resetAct); + + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(1, 1, 1, 1); + layout->setSpacing(1); + + layout->addStretch(); + layout->addWidget(resetBtn); + layout->addWidget(disconnectBtn); + layout->addStretch(); } const DBusClientPtr& client() const diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp index 5e2a52e2e..a2b5647df 100644 --- a/tests/gui/TestGuiFdoSecrets.cpp +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -608,6 +608,62 @@ void TestGuiFdoSecrets::testServiceUnlockItems() DBUS_COMPARE(item->locked(), false); } +void TestGuiFdoSecrets::testServiceUnlockItemsIncludeFutureEntries() +{ + 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); + + { + 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("")); + // remember and include future entries + VERIFY(driveAccessControlDialog(true, true)); + + 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); + } + + // check other entries are also unlocked + { + DBUS_GET(itemPaths, coll->items()); + VERIFY(itemPaths.size() > 1); + auto anotherItem = getProxy(itemPaths.last()); + VERIFY(anotherItem); + DBUS_COMPARE(anotherItem->locked(), false); + } +} + void TestGuiFdoSecrets::testServiceLock() { auto service = enableService(); @@ -1635,7 +1691,7 @@ TestGuiFdoSecrets::encryptPassword(QByteArray value, QString contentType, const return m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal(); } -bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember) +bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember, bool includeFutureEntries) { processEvents(); for (auto w : QApplication::topLevelWidgets()) { @@ -1647,7 +1703,13 @@ bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember) auto rememberCheck = dlg->findChild("rememberCheck"); VERIFY(rememberCheck); rememberCheck->setChecked(remember); - dlg->done(AccessControlDialog::AllowSelected); + + if (includeFutureEntries) { + dlg->done(AccessControlDialog::AllowAll); + } else { + dlg->done(AccessControlDialog::AllowSelected); + } + processEvents(); VERIFY(dlg->isHidden()); return true; diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h index 285619f86..03f84adce 100644 --- a/tests/gui/TestGuiFdoSecrets.h +++ b/tests/gui/TestGuiFdoSecrets.h @@ -71,6 +71,7 @@ private slots: void testServiceUnlock(); void testServiceUnlockDatabaseConcurrent(); void testServiceUnlockItems(); + void testServiceUnlockItemsIncludeFutureEntries(); void testServiceLock(); void testServiceLockConcurrent(); @@ -103,7 +104,7 @@ private slots: private: bool driveUnlockDialog(); bool driveNewDatabaseWizard(); - bool driveAccessControlDialog(bool remember = true); + bool driveAccessControlDialog(bool remember = true, bool includeFutureEntries = false); bool waitForSignal(QSignalSpy& spy, int expectedCount); void processEvents();