Allow groups to restrict by browser integration key (#6437)

This commit is contained in:
Michael Duersch 2023-09-17 16:21:05 -06:00 committed by Jonathan White
parent 7bfe9065cf
commit 884386c924
10 changed files with 205 additions and 7 deletions

View File

@ -953,7 +953,7 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePassXC - New key association request</source>
<source>Disable</source>
<translation type="unfinished"></translation>
</message>
<message>
@ -972,6 +972,10 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
<source>KeePassXC - Delete entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePassXC - New key association request</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BrowserSettingsWidget</name>
@ -3209,6 +3213,14 @@ Would you like to correct it?</source>
<source>Omit WWW subdomain from matching toggle for this and sub groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Restrict matching to given browser key:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Restrict matching to given browser key toggle for this and sub groups</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditGroupWidgetKeeShare</name>

View File

@ -64,6 +64,7 @@ const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEnt
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
const QString BrowserService::OPTION_RESTRICT_KEY = QStringLiteral("BrowserRestrictKey");
Q_GLOBAL_STATIC(BrowserService, s_browserService);
@ -947,6 +948,7 @@ bool BrowserService::deleteEntry(const QString& uuid)
QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
const QString& siteUrl,
const QString& formUrl,
const QStringList& keys,
bool passkey)
{
QList<Entry*> entries;
@ -961,6 +963,12 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
continue;
}
// If a key restriction is specified and not contained in the keys list then skip this group.
auto restrictKey = group->resolveCustomDataString(BrowserService::OPTION_RESTRICT_KEY);
if (!restrictKey.isEmpty() && !keys.contains(restrictKey)) {
continue;
}
const auto omitWwwSubdomain =
group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW) == Group::Enable;
@ -997,30 +1005,35 @@ QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
const StringPairList& keyList,
bool passkey)
{
// Check if database is connected with KeePassXC-Browser
// Check if database is connected with KeePassXC-Browser. If so, return browser key (otherwise empty)
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
for (const StringPair& keyPair : keyList) {
QString key = db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + keyPair.first);
if (!key.isEmpty() && keyPair.second == key) {
return true;
return keyPair.first;
}
}
return false;
return QString();
};
// Get the list of databases to search
QList<QSharedPointer<Database>> databases;
QStringList keys;
if (browserSettings()->searchInAllDatabases()) {
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
auto db = dbWidget->database();
if (db && databaseConnected(dbWidget->database())) {
auto key = databaseConnected(dbWidget->database());
if (db && !key.isEmpty()) {
databases << db;
keys << key;
}
}
} else {
const auto& db = getDatabase();
if (databaseConnected(db)) {
auto key = databaseConnected(db);
if (!key.isEmpty()) {
databases << db;
keys << key;
}
}
@ -1029,13 +1042,18 @@ QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
QList<Entry*> entries;
do {
for (const auto& db : databases) {
entries << searchEntries(db, siteUrl, formUrl, passkey);
entries << searchEntries(db, siteUrl, formUrl, keys, passkey);
}
} while (entries.isEmpty() && removeFirstDomain(hostname));
return entries;
}
QString BrowserService::decodeCustomDataRestrictKey(const QString& key)
{
return key.isEmpty() ? tr("Disable") : key;
}
void BrowserService::requestGlobalAutoType(const QString& search)
{
emit osUtils->globalShortcutTriggered("autotype", search);

View File

@ -119,6 +119,8 @@ public:
QJsonArray findEntries(const EntryParameters& entryParameters, const StringPairList& keyList, bool* entriesFound);
void requestGlobalAutoType(const QString& search);
static QString decodeCustomDataRestrictKey(const QString& key);
static const QString KEEPASSXCBROWSER_NAME;
static const QString KEEPASSXCBROWSER_OLD_NAME;
static const QString OPTION_SKIP_AUTO_SUBMIT;
@ -126,6 +128,7 @@ public:
static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
static const QString OPTION_RESTRICT_KEY;
signals:
void requestUnlock();
@ -157,6 +160,7 @@ private:
QList<Entry*> searchEntries(const QSharedPointer<Database>& db,
const QString& siteUrl,
const QString& formUrl,
const QStringList& keys = {},
bool passkey = false);
QList<Entry*>
searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList, bool passkey = false);

View File

@ -288,6 +288,21 @@ void Group::setCustomDataTriState(const QString& key, const Group::TriState& val
}
}
// Note that this returns an empty string both if the key is missing *or* if the key is present but value is empty.
QString Group::resolveCustomDataString(const QString& key, bool checkParent) const
{
// If not defined, check our parent up to the root group
if (!m_customData->contains(key)) {
if (!m_parent || !checkParent) {
return QString();
} else {
return m_parent->resolveCustomDataString(key);
}
}
return m_customData->value(key);
}
bool Group::equals(const Group* other, CompareItemOptions options) const
{
if (!other) {

View File

@ -102,6 +102,7 @@ public:
const CustomData* customData() const;
Group::TriState resolveCustomDataTriState(const QString& key, bool checkParent = true) const;
void setCustomDataTriState(const QString& key, const Group::TriState& value);
QString resolveCustomDataString(const QString& key, bool checkParent = true) const;
const Group* previousParentGroup() const;
QUuid previousParentGroupUuid() const;

View File

@ -196,6 +196,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
auto inheritOnlyHttp = false;
auto inheritNoHttp = false;
auto inheritOmitWww = false;
auto inheritRestrictKey = QString();
auto parent = group->parentGroup();
if (parent) {
@ -204,6 +205,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
inheritOnlyHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH);
inheritNoHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH);
inheritOmitWww = parent->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW);
inheritRestrictKey = parent->resolveCustomDataString(BrowserService::OPTION_RESTRICT_KEY);
}
// If the page has not been created at all, some of the elements are null
@ -219,6 +221,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
addTriStateItems(m_browserUi->browserIntegrationOnlyHttpAuthComboBox, inheritOnlyHttp);
addTriStateItems(m_browserUi->browserIntegrationNotHttpAuthComboBox, inheritNoHttp);
addTriStateItems(m_browserUi->browserIntegrationOmitWwwCombobox, inheritOmitWww);
addRestrictKeyComboBoxItems(m_db->metadata()->customData()->keys(), inheritRestrictKey);
m_browserUi->browserIntegrationHideEntriesComboBox->setCurrentIndex(
indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, false)));
@ -230,6 +233,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH, false)));
m_browserUi->browserIntegrationOmitWwwCombobox->setCurrentIndex(
indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW, false)));
setRestrictKeyComboBoxIndex(group);
} else if (hasPage(m_browserWidget)) {
setPageHidden(m_browserWidget, true);
}
@ -303,6 +307,7 @@ void EditGroupWidget::apply()
m_temporaryGroup->setCustomDataTriState(
BrowserService::OPTION_OMIT_WWW,
triStateFromIndex(m_browserUi->browserIntegrationOmitWwwCombobox->currentIndex()));
setRestrictKeyCustomData(m_temporaryGroup->customData());
}
#endif
@ -444,3 +449,58 @@ Group::TriState EditGroupWidget::triStateFromIndex(int index)
return Group::Inherit;
}
}
#ifdef WITH_XC_BROWSER
void EditGroupWidget::addRestrictKeyComboBoxItems(QStringList const& keyList, QString inheritValue)
{
auto comboBox = m_browserUi->browserIntegrationRestrictKeyCombobox;
comboBox->clear();
comboBox->addItem(
tr("Inherit from parent group (%1)").arg(BrowserService::decodeCustomDataRestrictKey(inheritValue)));
comboBox->addItem(tr("Disable"));
comboBox->insertSeparator(2);
// Add all the browser keys to the combobox
for (const QString& key : keyList) {
if (key.startsWith(CustomData::BrowserKeyPrefix)) {
auto strippedKey = key;
strippedKey.remove(CustomData::BrowserKeyPrefix);
comboBox->addItem(strippedKey);
}
}
}
void EditGroupWidget::setRestrictKeyComboBoxIndex(const Group* group)
{
auto comboBox = m_browserUi->browserIntegrationRestrictKeyCombobox;
if (!group || !group->customData()->contains(BrowserService::OPTION_RESTRICT_KEY)) {
comboBox->setCurrentIndex(0);
return;
}
auto key = group->customData()->value(BrowserService::OPTION_RESTRICT_KEY);
if (key.isEmpty()) {
comboBox->setCurrentIndex(1);
} else {
comboBox->setCurrentText(key);
}
}
// Set the customData regarding OPTION_RESTRICT_KEY
void EditGroupWidget::setRestrictKeyCustomData(CustomData* customData)
{
auto comboBox = m_browserUi->browserIntegrationRestrictKeyCombobox;
auto key = BrowserService::OPTION_RESTRICT_KEY;
auto idx = comboBox->currentIndex();
if (idx == 0) {
customData->remove(key);
} else if (idx == 1) {
customData->set(key, QString());
} else {
customData->set(key, comboBox->currentText());
}
}
#endif

View File

@ -81,6 +81,10 @@ private:
Group::TriState triStateFromIndex(int index);
void setupModifiedTracking();
void addRestrictKeyComboBoxItems(QStringList const& keyList, QString inheritValue);
void setRestrictKeyComboBoxIndex(const Group* group);
void setRestrictKeyCustomData(CustomData* customData);
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
QPointer<QScrollArea> m_editGroupWidgetMain;

View File

@ -136,6 +136,23 @@
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="browserIntegrationRestrictKey">
<property name="text">
<string>Restrict matching to given browser key:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="browserIntegrationRestrictKeyCombobox">
<property name="accessibleName">
<string>Restrict matching to given browser key toggle for this and sub groups</string>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer_1">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -158,6 +175,7 @@
<tabstop>browserIntegrationOnlyHttpAuthComboBox</tabstop>
<tabstop>browserIntegrationNotHttpAuthComboBox</tabstop>
<tabstop>browserIntegrationOmitWwwCombobox</tabstop>
<tabstop>browserIntegrationRestrictKeyCombobox</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -675,3 +675,68 @@ void TestBrowser::testBestMatchingWithAdditionalURLs()
QCOMPARE(sorted.length(), 1);
QCOMPARE(sorted[0]->url(), urls[0]);
}
void TestBrowser::testRestrictBrowserKey()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
// Group 0 (root): No browser key restriction given
QStringList urlsRoot = {"https://example.com/0"};
auto entriesRoot = createEntries(urlsRoot, root);
// Group 1: restricted to browser with 'key1'
auto* group1 = new Group();
group1->setParent(root);
group1->setName("TestGroup1");
group1->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key1");
QStringList urls1 = {"https://example.com/1"};
auto entries1 = createEntries(urls1, group1);
// Group 2: restricted to browser with 'key2'
auto* group2 = new Group();
group2->setParent(root);
group2->setName("TestGroup2");
group2->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key2");
QStringList urls2 = {"https://example.com/2"};
auto entries2 = createEntries(urls2, group2);
// Group 2b: inherits parent group (2) restriction
auto* group2b = new Group();
group2b->setParent(group2);
group2b->setName("TestGroup2b");
QStringList urls2b = {"https://example.com/2b"};
auto entries2b = createEntries(urls2b, group2b);
// Group 3: inherits parent group (root) - any browser can see
auto* group3 = new Group();
group3->setParent(root);
group3->setName("TestGroup3");
QStringList urls3 = {"https://example.com/3"};
auto entries3 = createEntries(urls3, group3);
// Browser 'key0': Groups 1 and 2 are excluded, so entries 0 and 3 will be found
auto siteUrl = QString("https://example.com");
auto result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key0"});
auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 2);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/0"));
// Browser 'key1': Group 2 will be excluded, so entries 0, 1, and 3 will be found
result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key1"});
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 3);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/1"));
QCOMPARE(sorted[2]->url(), QString("https://example.com/0"));
// Browser 'key2': Group 1 will be excluded, so entries 0, 2, 2b, 3 will be found
result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key2"});
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 4);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/2b"));
QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
}

View File

@ -49,6 +49,7 @@ private slots:
void testSubdomainsAndPaths();
void testBestMatchingCredentials();
void testBestMatchingWithAdditionalURLs();
void testRestrictBrowserKey();
private:
QList<Entry*> createEntries(QStringList& urls, Group* root) const;