diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170..e46916118 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -4616,6 +4616,25 @@ This will leave your passwords and sensitive information vulnerable! + + FdoSecrets::SettingsAliasesModel + + No database selected. + + + + The database with this UUID is not currently opened. + + + + Collection Alias + + + + Database + + + FdoSecrets::SettingsClientModel @@ -10227,6 +10246,23 @@ This option is deprecated, use --set-key-file instead. <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> + + Collection Aliases + + + + Add + + + + Remove + + + + Expose a database's exposed group under a specific alias. If the “default”-alias has not been configured here, it will point to the currently active database tab. +A database needs to be unlocked and have an exposed group configured to be selectable here. + + SettingsWidgetKeeShare diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 2c2b0bc57..c2f952d9c 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -196,6 +196,7 @@ static const QHash configStrings = { {Config::FdoSecrets_ConfirmDeleteItem, {QS("FdoSecrets/ConfirmDeleteItem"), Roaming, true}}, {Config::FdoSecrets_ConfirmAccessItem, {QS("FdoSecrets/ConfirmAccessItem"), Roaming, true}}, {Config::FdoSecrets_UnlockBeforeSearch, {QS("FdoSecrets/UnlockBeforeSearch"), Roaming, true}}, + {Config::FdoSecrets_CollectionAliasDatabaseUUIDs, {QS("FdoSecrets/CollectionAliasDatabaseUUIDs"), Roaming, QVariantMap()}}, // KeeShare {Config::KeeShare_QuietSuccess, {QS("KeeShare/QuietSuccess"), Roaming, false}}, diff --git a/src/core/Config.h b/src/core/Config.h index 8f54f9c01..1409d422e 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -174,6 +174,7 @@ public: FdoSecrets_ConfirmDeleteItem, FdoSecrets_ConfirmAccessItem, FdoSecrets_UnlockBeforeSearch, + FdoSecrets_CollectionAliasDatabaseUUIDs, KeeShare_QuietSuccess, KeeShare_Own, diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp index d24bc69a1..30376c84b 100644 --- a/src/fdosecrets/FdoSecretsSettings.cpp +++ b/src/fdosecrets/FdoSecretsSettings.cpp @@ -104,4 +104,36 @@ namespace FdoSecrets db->metadata()->customData()->set(CustomData::FdoSecretsExposedGroup, group.toString()); } + QVariantMap FdoSecretsSettings::collectionAliases() const + { + return config()->get(Config::FdoSecrets_CollectionAliasDatabaseUUIDs).toMap(); + } + + void FdoSecretsSettings::setCollectionAliases(const QVariantMap& aliases) + { + config()->set(Config::FdoSecrets_CollectionAliasDatabaseUUIDs, aliases); + } + + void FdoSecretsSettings::setCollectionAlias(QString alias, QUuid publicUuid) + { + auto aliases = collectionAliases(); + auto it = std::as_const(aliases).lowerBound(alias); + if (it != aliases.cend() && *it == publicUuid) + return; + aliases.insert(std::move(it), std::move(alias), std::move(publicUuid)); + // always signals collectionAliasesChanged(), so return above if nothing changed + setCollectionAliases(std::move(aliases)); + } + + void FdoSecretsSettings::removeCollectionAlias(const QString& alias, const QUuid& publicUuid) + { + auto aliases = collectionAliases(); + auto it = aliases.find(alias); + if (it == aliases.end() || it->toUuid() != publicUuid) + return; + aliases.erase(std::move(it)); + // always signals collectionAliasesChanged(), so return above if nothing changed + setCollectionAliases(std::move(aliases)); + } + } // namespace FdoSecrets diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h index 31ab005f6..ea0b94437 100644 --- a/src/fdosecrets/FdoSecretsSettings.h +++ b/src/fdosecrets/FdoSecretsSettings.h @@ -20,6 +20,7 @@ #include #include +#include class Database; @@ -47,6 +48,11 @@ namespace FdoSecrets bool unlockBeforeSearch() const; void setUnlockBeforeSearch(bool unlockBeforeSearch); + QVariantMap collectionAliases() const; + void setCollectionAliases(const QVariantMap& aliases); + void setCollectionAlias(QString alias, QUuid publicUuuid); + void removeCollectionAlias(const QString& alias, const QUuid& publicUuid); + // Per db settings QUuid exposedGroup(const QSharedPointer& db) const; diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index e3fcefeb5..5641f041a 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -78,6 +78,11 @@ namespace FdoSecrets onDatabaseTabOpened(dbWidget, true); }); + // when a new database is opened, apply it's aliases + connect(m_databases.data(), &DatabaseTabWidget::databaseOpened, this, &Service::applyCollectionAliasSettings); + // apply aliases from settings, when they change + connect(config(), &Config::changed, this, &Service::handleSettingsChanged); + // make default alias track current activated database connect(m_databases.data(), &DatabaseTabWidget::activeDatabaseChanged, this, &Service::ensureDefaultAlias); @@ -163,7 +168,7 @@ namespace FdoSecrets void Service::ensureDefaultAlias() { - if (m_insideEnsureDefaultAlias) { + if (m_insideEnsureDefaultAlias || m_explicitlyDefinedDefaultAlias) { return; } @@ -463,6 +468,51 @@ namespace FdoSecrets return collection->addAlias(name); } + void Service::handleSettingsChanged(Config::ConfigKey key) + { + if (key == Config::FdoSecrets_CollectionAliasDatabaseUUIDs) { + applyCollectionAliasSettings(); + } + } + + void Service::applyCollectionAliasSettings() + { + auto aliases = settings()->collectionAliases(); + // add missing aliases + m_explicitlyDefinedDefaultAlias = false; // is there still an explicit default alias? + for (auto it = aliases.cbegin(); it != aliases.cend(); ++it) { + auto& alias = it.key(); + if (alias == DEFAULT_ALIAS) { + m_explicitlyDefinedDefaultAlias = true; // will disable this->ensureDefaultAlias() + } + + auto database = m_databases->databaseWidgetFromPublicUuid(it->toUuid()); + if (!database) + continue; // cannot add alias to database not currently opened + auto collection = m_dbToCollection.value(database, nullptr); + if (!collection) + continue; + + collection->addAlias(alias).okOrDie(); // is idempotent + } + // remove unexpected aliases + QList> aliasesToRemove = {}; + for (auto it = m_aliases.cbegin(); it != m_aliases.cend(); ++it) { + auto& alias = it.key(); + Collection* const isCollection = *it; // the collection this alias currently points to + auto shouldUuid = aliases.value(alias, {}).toUuid(); // the UUID this alias should point to + if (shouldUuid.isNull() || isCollection->backend()->database()->publicUuid() != shouldUuid) { + // don't call isCollection->removeAlias, to avoid invalidating the iterator it + aliasesToRemove.append(std::make_pair(alias, isCollection)); + } + } + for (auto toRemove : aliasesToRemove) { + toRemove.second->removeAlias(toRemove.first).okOrDie(); + } + // select default alias, if not explicitly defined + ensureDefaultAlias(); + } + Collection* Service::findCollection(const QString& alias) const { if (alias.isEmpty()) { @@ -495,11 +545,23 @@ namespace FdoSecrets { auto coll = qobject_cast(sender()); m_aliases[alias] = coll; + if (!(m_insideEnsureDefaultAlias && alias == DEFAULT_ALIAS)) { + // store dynamically created aliases to settings + settings()->setCollectionAlias(alias, coll->backend()->database()->publicUuid()); + } } void Service::onCollectionAliasRemoved(const QString& alias) { - m_aliases.remove(alias); + auto coll = m_aliases.take(alias); + if (coll + && !(m_insideEnsureDefaultAlias && alias == DEFAULT_ALIAS) + // don't persistently remove alias after collectionAboutToDelete() was emitted + && m_dbToCollection.contains(coll->backend())) { + // pass publicUuid, to avoid removing alias x to database A, when + // applyCollectionAliasSettings() removes x->B while applying this very x->A + settings()->removeCollectionAlias(alias, coll->backend()->database()->publicUuid()); + } ensureDefaultAlias(); } diff --git a/src/fdosecrets/objects/Service.h b/src/fdosecrets/objects/Service.h index 5ec7499b1..24068b854 100644 --- a/src/fdosecrets/objects/Service.h +++ b/src/fdosecrets/objects/Service.h @@ -18,6 +18,7 @@ #ifndef KEEPASSXC_FDOSECRETS_SERVICE_H #define KEEPASSXC_FDOSECRETS_SERVICE_H +#include "core/Config.h" #include "fdosecrets/dbus/DBusClient.h" #include "fdosecrets/dbus/DBusObject.h" @@ -143,6 +144,9 @@ namespace FdoSecrets void onCollectionAliasRemoved(const QString& alias); + void handleSettingsChanged(Config::ConfigKey key); + void applyCollectionAliasSettings(); + private: bool initialize(); @@ -173,6 +177,7 @@ namespace FdoSecrets QList m_sessions{}; bool m_insideEnsureDefaultAlias{false}; + bool m_explicitlyDefinedDefaultAlias{false}; bool m_unlockingAnyDatabase{false}; // list of db currently has unlock dialog shown QHash m_unlockingDb{}; diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index d68ee4b0c..3146b7697 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -25,6 +25,7 @@ #include "gui/Icons.h" #include +#include namespace FdoSecrets { @@ -244,6 +245,273 @@ namespace FdoSecrets } } + // static constexpr still requires definition before c++17 + constexpr const char* SettingsAliasesModel::ColumnNames[]; + + SettingsAliasesModel::SettingsAliasesModel(const DatabaseTabWidget* dbTabs, QObject* parent) + : QAbstractTableModel(parent) + , m_databases(dbTabs) + { + } + + void SettingsAliasesModel::setAliases(QVariantMap aliases) + { + beginResetModel(); + m_aliases = aliases; + endResetModel(); + } + + const QVariantMap& SettingsAliasesModel::aliases() const + { + return m_aliases; + } + + int SettingsAliasesModel::rowCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return newRowIndex() + extraNewRow; + } + + int SettingsAliasesModel::newRowIndex() const + { + return m_aliases.size(); + } + + int SettingsAliasesModel::columnCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return sizeof(ColumnNames) / sizeof(ColumnNames[0]); + } + + QVariant SettingsAliasesModel::headerData(int section, Qt::Orientation orientation, int role) const + { + if (orientation != Qt::Horizontal) { + return {}; + } + + if (role != Qt::DisplayRole) { + return {}; + } + + if (section < 0 || section >= columnCount({})) { + return {}; + } + + return qApp->translate(metaObject()->className(), ColumnNames[section]); + } + + QVariantMap::const_iterator SettingsAliasesModel::rowAlias(const QModelIndex& index) const + { + return std::next(m_aliases.cbegin(), index.row()); + } + + QVariantMap::iterator SettingsAliasesModel::rowAlias(const QModelIndex& index) + { + return std::next(m_aliases.begin(), index.row()); + } + + QVariant SettingsAliasesModel::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) { + return {}; + } + if (index.model() != this) { + return {}; + } + if (index.row() >= rowCount({}) || index.column() >= columnCount({})) { + return {}; + } + if (index.row() == newRowIndex()) { // final empty row, to allow adding more aliases + return {}; + } + + switch (index.column()) { + case ColumnAlias: + return dataForCollectionAlias(rowAlias(index).key(), role); + case ColumnDatabase: + return dataForDatabase(rowAlias(index)->toUuid(), role); + default: + return {}; + } + } + + QVariant SettingsAliasesModel::dataForCollectionAlias(const QString& alias, int role) const + { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: { + return alias; + } + default: + return {}; + } + } + + QVariant SettingsAliasesModel::dataForDatabase(const QUuid& publicUuid, int role) const + { + if (role == Qt::EditRole) { + return publicUuid; // initial value for editor for this cell + } + + auto dbWidget = m_databases->databaseWidgetFromPublicUuid(publicUuid); + if (dbWidget) { + auto db = dbWidget->database(); + switch (role) { + case Qt::DisplayRole: { + return dbWidget->displayName(); + } + case Qt::ToolTipRole: + return db->filePath(); + default: + return {}; + } + } else if (publicUuid.isNull()) { + switch (role) { + case Qt::DisplayRole: { + return tr("No database selected."); + } + case Qt::FontRole: { + QFont font; + font.setItalic(true); + return font; + } + default: + return {}; + } + } else { + switch (role) { + case Qt::DisplayRole: { + return publicUuid; + } + case Qt::ToolTipRole: + return tr("The database with this UUID is not currently opened."); + case Qt::FontRole: { + QFont font; + font.setItalic(true); + return font; + } + default: + return {}; + } + } + } + + void SettingsAliasesModel::moveAlias(const QModelIndex& index, + const QString& prevAlias, + QString nextAlias, + QVariant database) + { + const auto destIt = std::as_const(m_aliases).lowerBound(nextAlias); + // beginMoveRows takes that element's current index, in front of which we insert + auto destRowIdx = std::distance(m_aliases.cbegin(), destIt); + // inserting directly above or below the element to be removed doesn't move + const bool willMove = destRowIdx != index.row() && destRowIdx - 1 != index.row(); + if (willMove) { + // move index.row() to destRowIdx + beginMoveRows({}, index.row(), index.row(), {}, destRowIdx); + } + const bool newRow = index.row() == newRowIndex(); + m_aliases.insert(std::move(destIt), std::move(nextAlias), std::move(database)); + if (newRow) { + // remove the special "newRow", which does not correspond to m_aliases + extraNewRow = false; // cannot nest beginMoveRows and beginInsertRows + } else { + // remove a normal row, which does correspond to an m_aliases entry + m_aliases.remove(prevAlias); + } + if (willMove) { + // one insert and one remove -> we moved a row + endMoveRows(); + } + // during this move, we might have also changed the cell values + if (index.row() < destRowIdx) { + --destRowIdx; // shifted up, when we removed index.row() + } + emit dataChanged(this->index(destRowIdx, ColumnAlias), this->index(destRowIdx, ColumnDatabase)); + if (newRow) { + // create a new "newRow", if we (re)moved the old one + beginInsertRows({}, newRowIndex(), newRowIndex()); + extraNewRow = true; // increases this->rowCount() by one + endInsertRows(); + } + } + + bool SettingsAliasesModel::setData(const QModelIndex& index, const QVariant& value, int role) + { + switch (index.column()) { + case ColumnAlias: { + QString prevAlias = ""; + QString nextAlias = value.toString(); + QVariant database = QUuid(); + if (index.row() == newRowIndex()) { + if (nextAlias.isEmpty()) + return false; // don't add empty alias + if (m_aliases.contains(nextAlias)) + return false; // don't override existing alias + } else { + auto prevRow = rowAlias(index); + prevAlias = prevRow.key(); + database = prevRow.value(); + if (nextAlias.isEmpty()) { + beginRemoveRows({}, index.row(), index.row()); + m_aliases.erase(prevRow); + endRemoveRows(); + return true; // edit to empty -> remove row + } + if (nextAlias == prevAlias) { + return true; // editor didn't change this value + } + if (m_aliases.contains(nextAlias)) { + return false; // refuse duplicate alias + } + } + moveAlias(index, prevAlias, std::move(nextAlias), std::move(database)); + return true; + } + case ColumnDatabase: { + auto nextDatabase = value.toUuid(); + QString alias = "default"; + if (index.row() == newRowIndex()) { + // find a new unique alias name + int i = 1; + while (m_aliases.contains(alias)) { + alias = QString("alias%1").arg(i++); + } + // insert new alias for selected database + moveAlias(index, "", std::move(alias), std::move(nextDatabase)); + } else { + // just update this value (won't move/insert/remove any rows) + *rowAlias(index) = nextDatabase; + emit dataChanged(index, index); + } + return true; + } + default: + return QAbstractTableModel::setData(index, value, role); + } + } + + Qt::ItemFlags SettingsAliasesModel::flags(const QModelIndex& index) const + { + if (!index.isValid() || index.model() != this || index.row() >= rowCount({}) + || index.column() >= columnCount({})) { + return {}; + } + // all table cells are editable (see SettingsAliasesModel::setData()) + return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; + } + + void SettingsAliasesModel::removeRow(int row) + { + beginRemoveRows({}, row, row); + m_aliases.erase(std::next(m_aliases.begin(), row)); + endRemoveRows(); + } + // static constexpr still requires definition before c++17 constexpr const char* SettingsClientModel::ColumnNames[]; diff --git a/src/fdosecrets/widgets/SettingsModels.h b/src/fdosecrets/widgets/SettingsModels.h index a6cdf41de..c9de862c4 100644 --- a/src/fdosecrets/widgets/SettingsModels.h +++ b/src/fdosecrets/widgets/SettingsModels.h @@ -21,6 +21,7 @@ #include "fdosecrets/dbus/DBusClient.h" #include +#include class DatabaseTabWidget; class DatabaseWidget; @@ -72,6 +73,48 @@ namespace FdoSecrets class DBusMgr; + class SettingsAliasesModel : public QAbstractTableModel + { + Q_OBJECT + public: + explicit SettingsAliasesModel(const DatabaseTabWidget* dbTabs, QObject* parent = nullptr); + + void setAliases(QVariantMap aliases); + const QVariantMap& aliases() const; + void removeRow(int row); + + int newRowIndex() const; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + enum Column + { + ColumnAlias, + ColumnDatabase, + }; + static constexpr const char* ColumnNames[] = { + QT_TRANSLATE_NOOP("FdoSecrets::SettingsAliasesModel", "Collection Alias"), + QT_TRANSLATE_NOOP("FdoSecrets::SettingsAliasesModel", "Database"), + }; + + private: + QVariantMap::const_iterator rowAlias(const QModelIndex&) const; + QVariantMap::iterator rowAlias(const QModelIndex&); + void moveAlias(const QModelIndex& index, const QString& prevAlias, QString nextAlias, QVariant database); + QVariant dataForCollectionAlias(const QString& alias, int role) const; + QVariant dataForDatabase(const QUuid& publicUuid, int role) const; + + private: + const DatabaseTabWidget* m_databases; + QVariantMap m_aliases; + // whether to include an empty row at the bottom, for new alias entry + bool extraNewRow = true; + }; + class SettingsClientModel : public QAbstractTableModel { Q_OBJECT diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 692a7561b..7e6a8ad33 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -16,6 +16,7 @@ */ #include "SettingsWidgetFdoSecrets.h" +#include "gui/DatabaseTabWidget.h" #include "ui_SettingsWidgetFdoSecrets.h" #include "fdosecrets/FdoSecretsPlugin.h" @@ -27,9 +28,11 @@ #include "gui/DatabaseWidget.h" #include +#include #include using FdoSecrets::DBusClientPtr; +using FdoSecrets::SettingsAliasesModel; using FdoSecrets::SettingsClientModel; using FdoSecrets::SettingsDatabaseModel; @@ -215,6 +218,62 @@ namespace }; } // namespace +class DatabaseUuidDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + DatabaseUuidDelegate(DatabaseTabWidget* dbTabs, QObject* parent = nullptr) + : QStyledItemDelegate(parent) + , m_dbTabs(dbTabs) + { + } + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex&) const override + { + QComboBox* e = new QComboBox(parent); + // Add existing database tabs + for (auto i = 0; i < m_dbTabs->count(); ++i) { + auto dbWidget = m_dbTabs->databaseWidgetFromIndex(i); + auto db = dbWidget->database(); + if (!FdoSecrets::settings()->exposedGroup(db).isNull()) { + auto idx = e->count(); + e->insertItem(idx, dbWidget->displayName(), db->publicUuid()); + e->setItemData(idx, db->filePath(), Qt::ToolTipRole); + } + } + return e; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + const QVariant current = index.model()->data(index, Qt::EditRole); + if (current.toUuid().isNull()) + return; // no previously selected db + + auto e = widget(editor); + auto idx = e->findData(current); + if (idx < 0) { // UUID of not currently opened db in model, add to editor + idx = e->count(); + e->insertItem(idx, current.toUuid().toString(), current); + } + e->setCurrentIndex(idx); + } + + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override + { + model->setData(index, widget(editor)->currentData()); + } + +private: + static QComboBox* widget(QWidget* editor) + { + return static_cast(editor); + } + + const DatabaseTabWidget* const m_dbTabs; +}; + SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) : QWidget(parent) , m_ui(new Ui::SettingsWidgetFdoSecrets()) @@ -224,6 +283,33 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi m_ui->warningMsg->setHidden(true); m_ui->warningMsg->setCloseButtonVisible(false); + m_aliasesModel = new SettingsAliasesModel(plugin->dbTabs(), this); + m_ui->tableAliases->setModel(m_aliasesModel); + + auto databaseDelegate = new DatabaseUuidDelegate(plugin->dbTabs(), this); + m_ui->tableAliases->setItemDelegateForColumn(SettingsAliasesModel::ColumnDatabase, databaseDelegate); + + connect(m_ui->addAliasButton, &QPushButton::clicked, [&]() { + auto index = + m_aliasesModel->index(m_aliasesModel->newRowIndex(), FdoSecrets::SettingsAliasesModel::ColumnAlias); + m_ui->tableAliases->setCurrentIndex(index); + m_ui->tableAliases->edit(index); + }); + connect(m_ui->removeAliasButton, &QPushButton::clicked, [&]() { + m_aliasesModel->removeRow(m_ui->tableAliases->currentIndex().row()); + }); + connect(m_ui->tableAliases->selectionModel(), + &QItemSelectionModel::currentChanged, + [&](const QModelIndex& current, const QModelIndex&) { + m_ui->removeAliasButton->setEnabled(current.isValid()); + }); + + auto aliasesViewHeader = m_ui->tableAliases->horizontalHeader(); + aliasesViewHeader->setSelectionMode(QAbstractItemView::NoSelection); + aliasesViewHeader->setSectionsClickable(false); + aliasesViewHeader->setSectionResizeMode(QHeaderView::ResizeToContents); + aliasesViewHeader->setSectionResizeMode(SettingsAliasesModel::ColumnDatabase, QHeaderView::Stretch); + auto clientModel = new SettingsClientModel(*plugin->dbus(), this); m_ui->tableClients->setModel(clientModel); installWidgetItemDelegate(m_ui->tableClients, @@ -273,6 +359,7 @@ void SettingsWidgetFdoSecrets::loadSettings() m_ui->confirmDeleteItem->setChecked(FdoSecrets::settings()->confirmDeleteItem()); m_ui->confirmAccessItem->setChecked(FdoSecrets::settings()->confirmAccessItem()); m_ui->unlockBeforeSearch->setChecked(FdoSecrets::settings()->unlockBeforeSearch()); + m_aliasesModel->setAliases(FdoSecrets::settings()->collectionAliases()); } void SettingsWidgetFdoSecrets::saveSettings() @@ -282,6 +369,7 @@ void SettingsWidgetFdoSecrets::saveSettings() FdoSecrets::settings()->setConfirmDeleteItem(m_ui->confirmDeleteItem->isChecked()); FdoSecrets::settings()->setConfirmAccessItem(m_ui->confirmAccessItem->isChecked()); FdoSecrets::settings()->setUnlockBeforeSearch(m_ui->unlockBeforeSearch->isChecked()); + FdoSecrets::settings()->setCollectionAliases(m_aliasesModel->aliases()); } void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index df7bcf7ff..6ab38bea0 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -20,11 +20,16 @@ #include "gui/MessageWidget.h" +#include #include class QAbstractItemView; class FdoSecretsPlugin; +namespace FdoSecrets +{ + class SettingsAliasesModel; +} namespace Ui { @@ -52,6 +57,7 @@ protected: private: QScopedPointer m_ui; FdoSecretsPlugin* m_plugin; + QPointer m_aliasesModel; QTimer m_checkTimer; }; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index 9c7ddb007..4a79abf4c 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -139,6 +139,78 @@ + + + Collection Aliases + + + + + + Expose a database's exposed group under a specific alias. If the “default”-alias has not been configured here, it will point to the currently active database tab. +A database needs to be unlocked and have an exposed group configured to be selectable here. + + + true + + + + + + + + + Qt::StrongFocus + + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + + + + + + + Add + + + + + + + false + + + Remove + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + + + + + + Authorization diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 2bf18071d..36903bb08 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -225,6 +225,13 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou { Q_ASSERT(dbWidget->database()); + // register public UUID before emiting databaseOpened() signal + auto publicUuid = dbWidget->database()->publicUuid(); + if (!m_publicUuidMap.contains(publicUuid)) { + // if two DBs have identical publicUuid, ignore the second one + m_publicUuidMap.insert(dbWidget->database()->publicUuid(), dbWidget); + } + // emit before index change emit databaseOpened(dbWidget); @@ -235,6 +242,7 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou setCurrentIndex(index); } + connect(dbWidget, &DatabaseWidget::databaseReplaced, this, &DatabaseTabWidget::updatePublicUuid); connect(dbWidget, SIGNAL(requestOpenDatabase(QString, bool, QString, QString)), SLOT(addDatabaseTab(QString, bool, QString, QString))); @@ -385,6 +393,11 @@ bool DatabaseTabWidget::closeDatabaseTab(DatabaseWidget* dbWidget) return false; } + auto publicUuid = dbWidget->database()->publicUuid(); + if (m_publicUuidMap.value(publicUuid) == dbWidget) { + m_publicUuidMap.remove(publicUuid); + } + QString filePath = dbWidget->database()->filePath(); if (!dbWidget->close()) { return false; @@ -685,6 +698,11 @@ void DatabaseTabWidget::updateTabName(int index) emit tabNameChanged(); } +DatabaseWidget* DatabaseTabWidget::databaseWidgetFromPublicUuid(const QUuid& publicUuid) const +{ + return m_publicUuidMap.value(publicUuid, nullptr); +} + DatabaseWidget* DatabaseTabWidget::databaseWidgetFromIndex(int index) const { return qobject_cast(widget(index)); @@ -886,6 +904,16 @@ void DatabaseTabWidget::updateLastDatabases() } } +void DatabaseTabWidget::updatePublicUuid(const QSharedPointer& oldDb, const QSharedPointer& newDb) +{ + if (!oldDb.isNull()) { + auto widget = m_publicUuidMap.take(oldDb->publicUuid()); + if (widget == sender()) { + m_publicUuidMap.insert(newDb->publicUuid(), widget); + } + } +} + void DatabaseTabWidget::emitActiveDatabaseChanged() { emit activeDatabaseChanged(currentDatabaseWidget()); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index a5074a84c..481f4fb89 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -43,6 +43,7 @@ public: QString tabName(int index); DatabaseWidget* currentDatabaseWidget(); DatabaseWidget* databaseWidgetFromIndex(int index) const; + DatabaseWidget* databaseWidgetFromPublicUuid(const QUuid& publicUuid) const; bool canSave(int index = -1) const; bool isModified(int index = -1) const; @@ -114,6 +115,7 @@ private slots: void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget); void handleExportError(const QString& reason); void updateLastDatabases(); + void updatePublicUuid(const QSharedPointer& oldDb, const QSharedPointer& newDb); private: QSharedPointer execNewDatabaseWizard(); @@ -127,6 +129,7 @@ private: QPointer m_importWizard; QTimer m_lockDelayTimer; bool m_databaseOpenInProgress; + QHash m_publicUuidMap; }; #endif // KEEPASSX_DATABASETABWIDGET_H diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp index fc7e218ea..319d26a36 100644 --- a/tests/gui/TestGuiFdoSecrets.cpp +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -29,6 +29,7 @@ #include "core/Global.h" #include "core/Tools.h" #include "crypto/Crypto.h" +#include "fdosecrets/widgets/SettingsModels.h" #include "gui/Application.h" #include "gui/DatabaseTabWidget.h" #include "gui/FileDialog.h" @@ -43,6 +44,7 @@ #include #include #include +#include #include int main(int argc, char* argv[]) @@ -202,6 +204,7 @@ void TestGuiFdoSecrets::init() VERIFY(m_dbWidget->save()); // enforce consistent default settings at the beginning + FdoSecrets::settings()->setCollectionAliases({}); FdoSecrets::settings()->setUnlockBeforeSearch(false); FdoSecrets::settings()->setShowNotification(false); FdoSecrets::settings()->setConfirmAccessItem(false); @@ -212,6 +215,7 @@ void TestGuiFdoSecrets::init() void TestGuiFdoSecrets::cleanup() { // restore to default settings + FdoSecrets::settings()->setCollectionAliases({}); FdoSecrets::settings()->setUnlockBeforeSearch(false); FdoSecrets::settings()->setShowNotification(false); FdoSecrets::settings()->setConfirmAccessItem(false); @@ -1602,6 +1606,196 @@ void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent() DBUS_COMPARE(coll->locked(), false); } +void TestGuiFdoSecrets::testConfiguredAlias() +{ + + const QString newalias = "newalias"; + + auto service = enableService(); + VERIFY(service); + + DBUS_GET(prePath, service->ReadAlias(newalias)); + COMPARE(prePath, QDBusObjectPath("/")); + + FdoSecrets::settings()->setCollectionAlias(newalias, m_db->publicUuid()); + + DBUS_GET(collPath, service->ReadAlias(newalias)); + auto coll = getProxy(collPath); + VERIFY(coll); + + FdoSecrets::settings()->removeCollectionAlias(newalias, m_db->publicUuid()); + + DBUS_GET(postPath, service->ReadAlias(newalias)); + COMPARE(postPath, QDBusObjectPath("/")); +} + +void TestGuiFdoSecrets::testConfiguredDefaultAlias() +{ + const QString defaultAlias = "default"; + + auto service = enableService(); + VERIFY(service); + + // read original default alias + DBUS_GET(oldCollPath, service->ReadAlias(defaultAlias)); + // create a second collection (selects the new db) + QDBusObjectPath newCollPath; + { + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); + + DBUS_GET2(collPath, + promptPath, + service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb")); + auto prompt = getProxy(promptPath); + DBUS_VERIFY(prompt->Prompt("")); + VERIFY(driveNewDatabaseWizard()); + + VERIFY(waitForSignal(spyCollectionCreated, 1)); + auto args = spyCollectionCreated.takeFirst(); + COMPARE(args.size(), 1); + newCollPath = args.at(0).value(); + } + VERIFY2(oldCollPath.path() != newCollPath.path(), oldCollPath.path().toStdString().data()); + COMPARE(m_tabWidget->count(), 2); + COMPARE(m_tabWidget->currentIndex(), 1); + + // read original default alias + DBUS_GET(origDefaultPath, service->ReadAlias(defaultAlias)); + COMPARE(origDefaultPath, newCollPath); + + // change default alias (back to the old db) + FdoSecrets::settings()->setCollectionAlias(defaultAlias, m_db->publicUuid()); + + // verify configuration had effect + DBUS_GET(configuredDefaultPath, service->ReadAlias(defaultAlias)); + COMPARE(configuredDefaultPath, oldCollPath); + auto configuredDefaultColl = getProxy(configuredDefaultPath); + VERIFY(configuredDefaultColl); + // verify, that the default alias no longer follows the active tab + m_tabWidget->setCurrentIndex(0); + DBUS_GET(configuredDefaultPath0, service->ReadAlias(defaultAlias)); + COMPARE(configuredDefaultPath0, oldCollPath); + m_tabWidget->setCurrentIndex(1); + DBUS_GET(configuredDefaultPath1, service->ReadAlias(defaultAlias)); + COMPARE(configuredDefaultPath1, oldCollPath); + + // remove default alias configuration + FdoSecrets::settings()->removeCollectionAlias(defaultAlias, m_db->publicUuid()); + + // verify that original default alias got restored + DBUS_GET(restoredDefaultPath, service->ReadAlias(defaultAlias)); + COMPARE(restoredDefaultPath, origDefaultPath); + // ... and changes with the active tab + m_tabWidget->setCurrentIndex(0); + DBUS_GET(restoredDefaultPath0, service->ReadAlias(defaultAlias)); + COMPARE(restoredDefaultPath0, oldCollPath); + m_tabWidget->setCurrentIndex(1); + DBUS_GET(restoredDefaultPath1, service->ReadAlias(defaultAlias)); + COMPARE(restoredDefaultPath1, newCollPath); +} + +void TestGuiFdoSecrets::testConfiguredUnavailableAlias() +{ + const QString alias = "unavailableAlias"; + auto service = enableService(); + VERIFY(service); + // configure an alias, for which the database is not currently opened + FdoSecrets::settings()->setCollectionAlias(alias, QUuid::createUuid()); + // check that this alias is not exposed + DBUS_GET(path, service->ReadAlias(alias)); + COMPARE(path, QDBusObjectPath("/")); +} + +void TestGuiFdoSecrets::testSettingsAliasesModelDisplay() +{ + FdoSecrets::SettingsAliasesModel model{m_tabWidget}; + QAbstractItemModelTester tester{&model}; + + model.setAliases({{"alias", m_db->publicUuid()}}); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole), m_dbWidget->displayName()); + QCOMPARE(model.data(model.index(0, 1), Qt::ToolTipRole), m_db->filePath()); + QCOMPARE(model.data(model.index(0, 1), Qt::EditRole), m_db->publicUuid()); +} + +void TestGuiFdoSecrets::testSettingsAliasesModel() +{ + FdoSecrets::SettingsAliasesModel model{m_tabWidget}; + QAbstractItemModelTester tester{&model}; + + const QVariant defaultUuid = QUuid::createUuid(); + const QVariant otherUuid = QUuid::createUuid(); + const QVariant newUuid = QUuid::createUuid(); + const QVariantMap origAliases{ + {"default", defaultUuid}, + {"otherAlias", otherUuid}, + }; + model.setAliases(origAliases); + QCOMPARE(model.aliases(), origAliases); + // last row should be empty (to be filled with new entries) + QCOMPARE(model.rowCount({}), origAliases.size() + 1); + const QModelIndex nextAliasIdx = model.index(origAliases.size(), 0); + const QModelIndex nextUuidIdx = model.index(origAliases.size(), 1); + QCOMPARE(model.data(nextAliasIdx, Qt::DisplayRole), QVariant()); + QCOMPARE(model.data(nextUuidIdx, Qt::DisplayRole), QVariant()); + // last row should be editable (to insert new entries) + QVERIFY(model.flags(nextAliasIdx) & Qt::ItemIsEditable); + QVERIFY(model.setData(nextAliasIdx, "newAlias")); + QVERIFY(model.aliases().contains("newAlias")); + // aliases sorted, so this should be newAlias + const QModelIndex newAliasIdx = model.index(1, 0); + const QModelIndex newUuidIdx = model.index(1, 1); + QCOMPARE(model.data(newAliasIdx, Qt::DisplayRole), "newAlias"); + QVERIFY(model.flags(newUuidIdx) & Qt::ItemIsEditable); + QVERIFY(model.setData(newUuidIdx, newUuid)); + QCOMPARE(model.aliases()["newAlias"], newUuid); + // entries can be renamed + QVERIFY(model.flags(newAliasIdx) & Qt::ItemIsEditable); + QCOMPARE(model.data(newAliasIdx, Qt::EditRole), "newAlias"); + QVERIFY(model.setData(newAliasIdx, "renamedAlias")); + QVERIFY(!model.aliases().contains("newAlias")); + // updates data model.aliases() + QCOMPARE(model.aliases()["default"], defaultUuid); + QCOMPARE(model.aliases()["otherAlias"], otherUuid); + QCOMPARE(model.aliases()["renamedAlias"], newUuid); + // updates sorted display + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole), "default"); + QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole), "otherAlias"); + QCOMPARE(model.data(model.index(2, 0), Qt::DisplayRole), "renamedAlias"); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole), defaultUuid); + QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole), otherUuid); + QCOMPARE(model.data(model.index(2, 1), Qt::DisplayRole), newUuid); + + // automatically generate names for newly inserted databases + const QVariant unnamedUuid = QUuid::createUuid(); + QVERIFY(model.setData(model.index(3, 1), unnamedUuid)); + QCOMPARE(model.aliases()["alias1"], unnamedUuid); // "default" already exists + // updates sorted display + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole), "alias1"); + QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole), "default"); + QCOMPARE(model.data(model.index(2, 0), Qt::DisplayRole), "otherAlias"); + QCOMPARE(model.data(model.index(3, 0), Qt::DisplayRole), "renamedAlias"); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole), unnamedUuid); + QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole), defaultUuid); + QCOMPARE(model.data(model.index(2, 1), Qt::DisplayRole), otherUuid); + QCOMPARE(model.data(model.index(3, 1), Qt::DisplayRole), newUuid); + + // remove rows + model.removeRow(1); + QCOMPARE(model.rowCount({}), 4); + QVERIFY(!model.aliases().contains("default")); + // other way of removing rows + QVERIFY(model.setData(model.index(1, 0), "")); + QCOMPARE(model.rowCount({}), 3); + QVERIFY(!model.aliases().contains("otherAlias")); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole), "alias1"); + QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole), "renamedAlias"); + QCOMPARE(model.data(model.index(2, 0), Qt::DisplayRole), {}); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole), unnamedUuid); + QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole), newUuid); + QCOMPARE(model.data(model.index(2, 1), Qt::DisplayRole), {}); +} + void TestGuiFdoSecrets::testExposeSubgroup() { auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup"); diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h index 1624eed49..55754842d 100644 --- a/tests/gui/TestGuiFdoSecrets.h +++ b/tests/gui/TestGuiFdoSecrets.h @@ -96,6 +96,12 @@ private slots: void testAlias(); void testDefaultAliasAlwaysPresent(); + void testConfiguredAlias(); + void testConfiguredDefaultAlias(); + void testConfiguredUnavailableAlias(); + + void testSettingsAliasesModelDisplay(); + void testSettingsAliasesModel(); void testExposeSubgroup(); void testModifyingExposedGroup();