From ad67eac25795772ca07e6b808994cf329219147d Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Thu, 11 Apr 2013 23:19:00 +0200 Subject: [PATCH] Confirmation dialog to list entries. --- src/CMakeLists.txt | 5 ++ src/http/AccessControlDialog.cpp | 52 ++++++++++++++ src/http/AccessControlDialog.h | 42 ++++++++++++ src/http/AccessControlDialog.ui | 69 +++++++++++++++++++ src/http/EntryConfig.cpp | 100 +++++++++++++++++++++++++++ src/http/EntryConfig.h | 54 +++++++++++++++ src/http/Service.cpp | 113 ++++++++++++++++++++++++------- src/http/Service.h | 2 + 8 files changed, 414 insertions(+), 23 deletions(-) create mode 100644 src/http/AccessControlDialog.cpp create mode 100644 src/http/AccessControlDialog.h create mode 100644 src/http/AccessControlDialog.ui create mode 100644 src/http/EntryConfig.cpp create mode 100644 src/http/EntryConfig.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4e031203c..b0425c3cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -101,6 +101,8 @@ set(keepassx_SOURCES gui/group/EditGroupWidget.cpp gui/group/GroupModel.cpp gui/group/GroupView.cpp + http/AccessControlDialog.cpp + http/EntryConfig.cpp http/Protocol.cpp http/Server.cpp http/Service.cpp @@ -166,6 +168,8 @@ set(keepassx_MOC gui/group/EditGroupWidget.h gui/group/GroupModel.h gui/group/GroupView.h + http/AccessControlDialog.h + http/EntryConfig.h http/Protocol.h http/Server.h http/Service.h @@ -196,6 +200,7 @@ set(keepassx_FORMS gui/entry/EditEntryWidgetHistory.ui gui/entry/EditEntryWidgetMain.ui gui/group/EditGroupWidgetMain.ui + http/AccessControlDialog.ui ) if(MINGW) diff --git a/src/http/AccessControlDialog.cpp b/src/http/AccessControlDialog.cpp new file mode 100644 index 000000000..de6f0e6ba --- /dev/null +++ b/src/http/AccessControlDialog.cpp @@ -0,0 +1,52 @@ +/** + *************************************************************************** + * @file AccessControlDialog.cpp + * + * @brief + * + * Copyright (C) 2013 + * + * @author Francois Ferrand + * @date 4/2013 + *************************************************************************** + */ + +#include "AccessControlDialog.h" +#include "ui_AccessControlDialog.h" +#include "core/Entry.h" + +AccessControlDialog::AccessControlDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AccessControlDialog) +{ + ui->setupUi(this); + connect(ui->allowButton, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->denyButton, SIGNAL(clicked()), this, SLOT(reject())); +} + +AccessControlDialog::~AccessControlDialog() +{ + delete ui; +} + +void AccessControlDialog::setUrl(const QString &url) +{ + ui->label->setText(QString(tr("%1 has requested access to passwords for the following item(s).\n" + "Please select whether you want to allow access.")).arg(QUrl(url).host())); +} + +void AccessControlDialog::setItems(const QList &items) +{ + Q_FOREACH (Entry * entry, items) + ui->itemsList->addItem(entry->title() + " - " + entry->username()); +} + +bool AccessControlDialog::remember() const +{ + return ui->rememberDecisionCheckBox->isChecked(); +} + +void AccessControlDialog::setRemember(bool r) +{ + ui->rememberDecisionCheckBox->setChecked(r); +} diff --git a/src/http/AccessControlDialog.h b/src/http/AccessControlDialog.h new file mode 100644 index 000000000..67a6a8492 --- /dev/null +++ b/src/http/AccessControlDialog.h @@ -0,0 +1,42 @@ +/** + *************************************************************************** + * @file AccessControlDialog.h + * + * @brief + * + * Copyright (C) 2013 + * + * @author Francois Ferrand + * @date 4/2013 + *************************************************************************** + */ + +#ifndef ACCESSCONTROLDIALOG_H +#define ACCESSCONTROLDIALOG_H + +#include + +class Entry; + +namespace Ui { +class AccessControlDialog; +} + +class AccessControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AccessControlDialog(QWidget *parent = 0); + ~AccessControlDialog(); + + void setUrl(const QString & url); + void setItems(const QList & items); + bool remember() const; + void setRemember(bool r); + +private: + Ui::AccessControlDialog *ui; +}; + +#endif // ACCESSCONTROLDIALOG_H diff --git a/src/http/AccessControlDialog.ui b/src/http/AccessControlDialog.ui new file mode 100644 index 000000000..4faed6de1 --- /dev/null +++ b/src/http/AccessControlDialog.ui @@ -0,0 +1,69 @@ + + + AccessControlDialog + + + + 0 + 0 + 400 + 221 + + + + KeyPassX/Http: Confirm Access + + + + + + + + + + + + + + + + Remember this decision + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Allow + + + + + + + Deny + + + + + + + + + + diff --git a/src/http/EntryConfig.cpp b/src/http/EntryConfig.cpp new file mode 100644 index 000000000..d5a518755 --- /dev/null +++ b/src/http/EntryConfig.cpp @@ -0,0 +1,100 @@ +/** + *************************************************************************** + * @file EntryConfig.cpp + * + * @brief + * + * Copyright (C) 2013 + * + * @author Francois Ferrand + * @date 4/2013 + *************************************************************************** + */ + +#include "EntryConfig.h" +#include "core/Entry.h" +#include "core/EntryAttributes.h" +#include "qjson/parser.h" +#include "qjson/qobjecthelper.h" +#include "qjson/serializer.h" + +static const char KEEPASSHTTP_NAME[] = "KeePassHttp Settings"; //TODO: duplicated string (also in Service.cpp) + +EntryConfig::EntryConfig(QObject *parent) : + QObject(parent) +{ +} + +QStringList EntryConfig::allowedHosts() const +{ + return m_allowedHosts.toList(); +} + +void EntryConfig::setAllowedHosts(const QStringList &allowedHosts) +{ + m_allowedHosts = allowedHosts.toSet(); +} + +QStringList EntryConfig::deniedHosts() const +{ + return m_deniedHosts.toList(); +} + +void EntryConfig::setDeniedHosts(const QStringList &deniedHosts) +{ + m_deniedHosts = deniedHosts.toSet(); +} + +bool EntryConfig::isAllowed(const QString &host) +{ + return m_allowedHosts.contains(host); +} + +void EntryConfig::allow(const QString &host) +{ + m_allowedHosts.insert(host); + m_deniedHosts.remove(host); +} + +bool EntryConfig::isDenied(const QString &host) +{ + return m_deniedHosts.contains(host); +} + +void EntryConfig::deny(const QString &host) +{ + m_deniedHosts.insert(host); + m_allowedHosts.remove(host); +} + +QString EntryConfig::realm() const +{ + return m_realm; +} + +void EntryConfig::setRealm(const QString &realm) +{ + m_realm = realm; +} + +bool EntryConfig::load(const Entry *entry) +{ + QString s = entry->attributes()->value(KEEPASSHTTP_NAME); + if (s.isEmpty()) + return false; + + bool isOk = false; + QVariant v = QJson::Parser().parse(s.toUtf8(), &isOk); + if (!isOk || !v.type() == QVariant::Map) + return false; + + QJson::QObjectHelper::qvariant2qobject(v.toMap(), this); + return true; +} + +void EntryConfig::save(Entry *entry) +{ + QVariant v = QJson::QObjectHelper::qobject2qvariant(this, QJson::QObjectHelper::Flag_None); + QByteArray json = QJson::Serializer().serialize(v); + entry->attributes()->set(KEEPASSHTTP_NAME, json); +} diff --git a/src/http/EntryConfig.h b/src/http/EntryConfig.h new file mode 100644 index 000000000..40633162f --- /dev/null +++ b/src/http/EntryConfig.h @@ -0,0 +1,54 @@ +/** + *************************************************************************** + * @file EntryConfig.h + * + * @brief + * + * Copyright (C) 2013 + * + * @author Francois Ferrand + * @date 4/2013 + *************************************************************************** + */ + +#ifndef ENTRYCONFIG_H +#define ENTRYCONFIG_H + +#include +#include +#include +#include + +class Entry; + +class EntryConfig : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList Allow READ allowedHosts WRITE setAllowedHosts) + Q_PROPERTY(QStringList Deny READ deniedHosts WRITE setDeniedHosts ) + Q_PROPERTY(QString Realm READ realm WRITE setRealm ) + +public: + EntryConfig(QObject * object = 0); + + bool load(const Entry * entry); + void save(Entry * entry); + bool isAllowed(const QString & host); + void allow(const QString & host); + bool isDenied(const QString & host); + void deny(const QString & host); + QString realm() const; + void setRealm(const QString &realm); + +private: + QStringList allowedHosts() const; + void setAllowedHosts(const QStringList &allowedHosts); + QStringList deniedHosts() const; + void setDeniedHosts(const QStringList &deniedHosts); + + QSet m_allowedHosts; + QSet m_deniedHosts; + QString m_realm; +}; + +#endif // ENTRYCONFIG_H diff --git a/src/http/Service.cpp b/src/http/Service.cpp index 4ade6169f..2e85932bc 100644 --- a/src/http/Service.cpp +++ b/src/http/Service.cpp @@ -11,17 +11,22 @@ *************************************************************************** */ +#include +#include +#include + #include "Service.h" #include "Protocol.h" +#include "EntryConfig.h" +#include "AccessControlDialog.h" + #include "core/Database.h" #include "core/Entry.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/Uuid.h" #include "core/PasswordGenerator.h" -#include -#include -#include + Service::Service(DatabaseTabWidget *parent) : KeepassHttpProtocol::Server(parent), @@ -121,8 +126,8 @@ QString Service::storeKey(const QString &key) //Indicate who wants to associate, and request user to enter the 'name' of association key id = QInputDialog::getText(0, tr("KeyPassX/Http: New key association request"), tr("You have received an association request for the above key. If you would like to " - "allow it access to your KeePassX database give it a unique name to identify and a" - "ccept it."), + "allow it access to your KeePassX database give it a unique name to identify and" + "accept it."), QLineEdit::Normal, QString(), &ok); if (!ok || id.isEmpty()) return QString(); @@ -182,31 +187,83 @@ QList Service::searchEntries(const QString &text) return entries; } +Service::Access Service::checkAccess(const Entry *entry, const QString & host, const QString & submitHost, const QString & realm) +{ + EntryConfig config; + if (!config.load(entry)) + return Unknown; //not configured + if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) + return Allowed; //allowed + if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) + return Denied; //denied + if (!realm.isEmpty() && config.realm() != realm) + return Denied; + return Unknown; //not configured for this host +} + QList Service::findMatchingEntries(const QString &id, const QString &url, const QString &submitUrl, const QString &realm) { QList result; + QList pwEntriesToConfirm; + + bool autoAccept = false; //TODO: setting! + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); const QList pwEntries = searchEntries(url); - Q_FOREACH (Entry * entry, pwEntries) { - //Filter accepted/denied entries - // if (c.Allow.Contains(formHost) && (submitHost == null || c.Allow.Contains(submitHost))) - // return true; - // if (c.Deny.Contains(formHost) || (submitHost != null && c.Deny.Contains(submitHost))) - // return false; - // if (realm != null && c.Realm != realm) - // return false; - //If we are unsure for some entries: - //- balloon to grant accessc if possible - //- if clicked, show confirmation dialog --> accept/reject (w/ list of items?) - // The website XXX wants to access your credentials - // MORE (---> if clicked, shows the list of returned entries) - // [x] Ask me again [Allow] [Deny] - // If accepted, store that entry can be accessed without confirmation - //- else, show only items which do not require validation + //Check entries for authorization + Q_FOREACH (Entry * entry, pwEntries) { + switch(checkAccess(entry, host, submitHost, realm)) { + case Denied: + continue; - //TODO: sort [--> need a flag], or do this in Server class [--> need an extra 'sort order' key in Entry, and we always compute it] - result << KeepassHttpProtocol::Entry(entry->title(), entry->username(), entry->password(), entry->uuid().toHex()); + case Unknown: + if (!autoAccept) { + pwEntriesToConfirm.append(entry); + break; + } + //fall through + case Allowed: + result << KeepassHttpProtocol::Entry(entry->title(), entry->username(), entry->password(), entry->uuid().toHex()); + break; + } } + + //If unsure, ask user for confirmation + if (!pwEntriesToConfirm.isEmpty()) { + //TODO: balloon to grant access + timeout + + AccessControlDialog dlg; + dlg.setUrl(url); + dlg.setItems(pwEntriesToConfirm); + //dlg.setRemember(); //TODO: setting! + + int res = dlg.exec(); + if (dlg.remember()) { + Q_FOREACH (Entry * entry, pwEntries) { + EntryConfig config; + config.load(entry); + if (res == QDialog::Accepted) { + config.allow(host); + if (!submitHost.isEmpty() && host != submitHost) + config.allow(submitHost); + } else if (res == QDialog::Rejected) { + config.deny(host); + if (!submitHost.isEmpty() && host != submitHost) + config.deny(submitHost); + } + if (!realm.isEmpty()) + config.setRealm(realm); + config.save(entry); + } + } + if (res == QDialog::Accepted) { + Q_FOREACH (Entry * entry, pwEntries) + result << KeepassHttpProtocol::Entry(entry->title(), entry->username(), entry->password(), entry->uuid().toHex()); + } + } + + //TODO: sort [--> need a flag], or do this in Server class [--> need an extra 'sort order' key in Entry, and we always compute it] return result; } @@ -259,6 +316,16 @@ void Service::addEntry(const QString &id, const QString &login, const QString &p entry->setUsername(login); entry->setPassword(password); entry->setGroup(group); + + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + EntryConfig config; + config.allow(host); + if (!submitHost.isEmpty()) + config.allow(submitHost); + if (!realm.isEmpty()) + config.setRealm(realm); + config.save(entry); } } diff --git a/src/http/Service.h b/src/http/Service.h index 123815b89..871a49904 100644 --- a/src/http/Service.h +++ b/src/http/Service.h @@ -39,8 +39,10 @@ public: virtual QString generatePassword(); private: + enum Access { Denied, Unknown, Allowed}; Entry* getConfigEntry(bool create = false); bool matchUrlScheme(const QString &url); + Access checkAccess(const Entry *entry, const QString & host, const QString & submitHost, const QString & realm); bool removeFirstDomain(QString &hostname); Group *findCreateAddEntryGroup(); QList searchEntries(const QString &text);