QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
usersdialog.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
3#include "usersdialog.h"
4#include "qtpasssettings.h"
5#include "ui_usersdialog.h"
6#include <QApplication>
7#include <QCloseEvent>
8#include <QDateTime>
9#include <QKeyEvent>
10#include <QMessageBox>
11#include <QRegularExpression>
12#include <QSet>
13#include <QWidget>
14#include <utility>
15
16#ifdef QT_DEBUG
17#include "debughelper.h"
18#endif
24UsersDialog::UsersDialog(const QString &dir, QWidget *parent)
25 : QDialog(parent), ui(new Ui::UsersDialog), m_dir(dir) {
26
27 ui->setupUi(this);
28
29 restoreDialogState();
30 if (!loadGpgKeys()) {
31 connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
32 return;
33 }
34
35 loadRecipients();
36 populateList();
37
38 connectSignals();
39}
40
41void UsersDialog::connectSignals() {
42 connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
44 connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
45 connect(ui->listWidget, &QListWidget::itemChanged, this,
46 &UsersDialog::itemChange);
47
48 ui->lineEdit->setClearButtonEnabled(true);
49}
50
54void UsersDialog::restoreDialogState() {
55 QByteArray savedGeometry = QtPassSettings::getDialogGeometry("usersDialog");
56 bool hasSavedGeometry = !savedGeometry.isEmpty();
57 if (hasSavedGeometry) {
58 restoreGeometry(savedGeometry);
59 }
60 if (QtPassSettings::isDialogMaximized("usersDialog")) {
61 showMaximized();
62 } else if (hasSavedGeometry) {
63 move(QtPassSettings::getDialogPos("usersDialog"));
64 resize(QtPassSettings::getDialogSize("usersDialog"));
65 }
66}
67
68auto UsersDialog::loadGpgKeys() -> bool {
69 QList<UserInfo> users = QtPassSettings::getPass()->listKeys();
70 if (users.isEmpty()) {
71 QMessageBox::critical(parentWidget(), tr("Keylist missing"),
72 tr("Could not fetch list of available GPG keys"));
73 reject();
74 return false;
75 }
76
77 markSecretKeys(users);
78
79 m_userList = users;
80 return true;
81}
82
83void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
84 QList<UserInfo> secret_keys = QtPassSettings::getPass()->listKeys("", true);
85 QSet<QString> secretKeyIds;
86 for (const UserInfo &sec : secret_keys) {
87 secretKeyIds.insert(sec.key_id);
88 }
89 for (auto &user : users) {
90 if (secretKeyIds.contains(user.key_id)) {
91 user.have_secret = true;
92 }
93 }
94}
95
96void UsersDialog::loadRecipients() {
97 int count = 0;
98 QStringList recipients = QtPassSettings::getPass()->getRecipientString(
99 m_dir.isEmpty() ? "" : m_dir, " ", &count);
100
101 QList<UserInfo> selectedUsers =
102 QtPassSettings::getPass()->listKeys(recipients);
103 QSet<QString> selectedKeyIds;
104 for (const UserInfo &sel : selectedUsers) {
105 selectedKeyIds.insert(sel.key_id);
106 }
107 for (auto &user : m_userList) {
108 if (selectedKeyIds.contains(user.key_id)) {
109 user.enabled = true;
110 }
111 }
112
113 if (count > selectedUsers.size()) {
114 QStringList allRecipients = QtPassSettings::getPass()->getRecipientList(
115 m_dir.isEmpty() ? "" : m_dir);
116
117 // Use bulk lookup to resolve all recipients at once (single gpg call)
118 // This preserves the original email/UID resolution behavior
119 QList<UserInfo> resolvedKeys =
120 QtPassSettings::getPass()->listKeys(allRecipients);
121 // Track resolved recipients by their resolved key_id
122 QSet<QString> resolvedKeyIds;
123 for (const UserInfo &key : resolvedKeys) {
124 resolvedKeyIds.insert(key.key_id);
125 }
126
127 // Accept a recipient as resolved if GPG returned a key for it
128 // (either its exact key_id, or GPG resolved email/UID/fingerprint to it)
129 QSet<QString> resolvedRecipients;
130 for (const UserInfo &key : resolvedKeys) {
131 resolvedRecipients.insert(key.key_id);
132 // Also add the name (email/UID) of resolved keys as valid resolved tokens
133 // since GPG matched them to this key
134 if (!key.name.isEmpty()) {
135 resolvedRecipients.insert(
136 key.name.section('@', 0, 0)); // email local part
137 resolvedRecipients.insert(key.name); // full email/UID
138 }
139 }
140
141 for (const QString &recipient : allRecipients) {
142 if (!resolvedKeyIds.contains(recipient) &&
143 !resolvedRecipients.contains(recipient) &&
144 !selectedKeyIds.contains(recipient)) {
145 UserInfo i;
146 i.enabled = true;
147 i.key_id = recipient;
148 i.name = " ?? " + tr("Key not found in keyring");
149 m_userList.append(i);
150 }
151 }
152 }
153}
154
159
164 QtPassSettings::getPass()->Init(m_dir, m_userList);
165
166 QDialog::accept();
167}
168
173void UsersDialog::closeEvent(QCloseEvent *event) {
174 QtPassSettings::setDialogGeometry("usersDialog", saveGeometry());
175 if (!isMaximized()) {
176 QtPassSettings::setDialogPos("usersDialog", pos());
177 QtPassSettings::setDialogSize("usersDialog", size());
178 }
179 QtPassSettings::setDialogMaximized("usersDialog", isMaximized());
180 event->accept();
181}
182
188void UsersDialog::keyPressEvent(QKeyEvent *event) {
189 switch (event->key()) {
190 case Qt::Key_Escape:
191 ui->lineEdit->clear();
192 break;
193 default:
194 break;
195 }
196}
197
202void UsersDialog::itemChange(QListWidgetItem *item) {
203 if (!item) {
204 return;
205 }
206 bool ok = false;
207 const int index = item->data(Qt::UserRole).toInt(&ok);
208 if (!ok) {
209#ifdef QT_DEBUG
210 qWarning() << "UsersDialog::itemChange: invalid user index data for item";
211#endif
212 return;
213 }
214 if (index < 0 || index >= m_userList.size()) {
215#ifdef QT_DEBUG
216 qWarning() << "UsersDialog::itemChange: user index out of range:" << index
217 << "valid range is [0," << (m_userList.size() - 1) << "]";
218#endif
219 return;
220 }
221 m_userList[index].enabled = item->checkState() == Qt::Checked;
222}
223
229void UsersDialog::populateList(const QString &filter) {
230 // Invalidate cached datetime so expiry checks use fresh current time
231 m_cachedDateTimeValid = false;
232
233 QString patternString = "*" + filter + "*";
234 if (m_cachedPatternString != patternString) {
235 QRegularExpression re(
236 QRegularExpression::wildcardToRegularExpression(patternString),
237 QRegularExpression::CaseInsensitiveOption);
238 if (re.isValid()) {
239 m_cachedNameFilter = re;
240 m_cachedPatternString = patternString;
241 }
242 }
243 const QRegularExpression &nameFilter = m_cachedNameFilter;
244 ui->listWidget->clear();
245
246 for (int i = 0; i < m_userList.size(); ++i) {
247 const auto &user = m_userList.at(i);
248 if (!passesFilter(user, filter, nameFilter)) {
249 continue;
250 }
251
252 auto *item = new QListWidgetItem(buildUserText(user), ui->listWidget);
253 applyUserStyling(item, user);
254 item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked);
255 item->setData(Qt::UserRole, QVariant::fromValue(i));
256 ui->listWidget->addItem(item);
257 }
258}
259
267bool UsersDialog::passesFilter(const UserInfo &user, const QString &filter,
268 const QRegularExpression &nameFilter) const {
269 if (!filter.isEmpty() && !nameFilter.match(user.name).hasMatch()) {
270 return false;
271 }
272 if (!user.isValid() && !ui->checkBox->isChecked()) {
273 return false;
274 }
275 const bool expired = isUserExpired(user);
276 return !(expired && !ui->checkBox->isChecked());
277}
278
284auto UsersDialog::isUserExpired(const UserInfo &user) const -> bool {
285 if (!m_cachedDateTimeValid) {
286 m_cachedCurrentDateTime = QDateTime::currentDateTime();
287 m_cachedDateTimeValid = true;
288 }
289 return user.expiry.toSecsSinceEpoch() > 0 &&
290 m_cachedCurrentDateTime > user.expiry;
291}
292
298QString UsersDialog::buildUserText(const UserInfo &user) const {
299 QString text = user.name + "\n" + user.key_id;
300 if (user.created.toSecsSinceEpoch() > 0) {
301 text += " " + tr("created") + " " +
302 QLocale::system().toString(user.created, QLocale::ShortFormat);
303 }
304 if (user.expiry.toSecsSinceEpoch() > 0) {
305 text += " " + tr("expires") + " " +
306 QLocale::system().toString(user.expiry, QLocale::ShortFormat);
307 }
308 return text;
309}
310
316void UsersDialog::applyUserStyling(QListWidgetItem *item,
317 const UserInfo &user) const {
318 const QString originalText = item->text();
319 if (user.have_secret) {
320 const QPalette palette = QApplication::palette();
321 item->setForeground(palette.color(QPalette::Link));
322 QFont font = item->font();
323 font.setBold(true);
324 item->setFont(font);
325 } else if (!user.isValid()) {
326 item->setBackground(Qt::darkRed);
327 item->setForeground(Qt::white);
328 item->setText(tr("[INVALID] ") + originalText);
329 } else if (isUserExpired(user)) {
330 item->setForeground(Qt::darkRed);
331 item->setText(tr("[EXPIRED] ") + originalText);
332 } else if (!user.fullyValid()) {
333 item->setBackground(Qt::darkYellow);
334 item->setForeground(Qt::white);
335 item->setText(tr("[PARTIAL] ") + originalText);
336 } else {
337 item->setText(originalText);
338 }
339}
340
345void UsersDialog::on_lineEdit_textChanged(const QString &filter) {
346 populateList(filter);
347}
348
352void UsersDialog::on_checkBox_clicked() { populateList(ui->lineEdit->text()); }
static auto getDialogPos(const QString &key, const QPoint &defaultValue=QVariant().toPoint()) -> QPoint
Get saved dialog position.
static auto getPass() -> Pass *
Get currently active pass backend instance.
static void setDialogPos(const QString &key, const QPoint &pos)
Save dialog position.
static auto getDialogSize(const QString &key, const QSize &defaultValue=QVariant().toSize()) -> QSize
Get saved dialog size.
static void setDialogSize(const QString &key, const QSize &size)
Save dialog size.
static void setDialogMaximized(const QString &key, const bool &maximized)
Save dialog maximized state.
static auto isDialogMaximized(const QString &key, const bool &defaultValue=QVariant().toBool()) -> bool
Get dialog maximized state.
static void setDialogGeometry(const QString &key, const QByteArray &geometry)
Save dialog geometry.
static auto getDialogGeometry(const QString &key, const QByteArray &defaultValue=QVariant().toByteArray()) -> QByteArray
Get saved dialog geometry.
~UsersDialog() override
Destructor.
void keyPressEvent(QKeyEvent *event) override
Handle key press.
void accept() override
Handle dialog acceptance.
void closeEvent(QCloseEvent *event) override
Handle close event.
UsersDialog(const QString &dir, QWidget *parent=nullptr)
Construct users dialog.
Debug utilities for QtPass.
Stores key info lines including validity, creation date and more.
Definition userinfo.h:13
bool have_secret
UserInfo::have_secret whether secret key is available (can decrypt with this key).
Definition userinfo.h:51
bool enabled
UserInfo::enabled Whether this user/key is enabled for normal use. True when the key should be treate...
Definition userinfo.h:58
QString key_id
UserInfo::key_id hexadecimal representation of the GnuPG key identifier.
Definition userinfo.h:41
auto fullyValid() const -> bool
UserInfo::fullyValid when validity is f or u. http://git.gnupg.org/cgi-bin/gitweb....
Definition userinfo.h:20
auto isValid() const -> bool
UserInfo::isValid when fullyValid or marginallyValid.
Definition userinfo.h:31
QDateTime created
UserInfo::created date/time when key was created.
Definition userinfo.h:66
QString name
UserInfo::name GPG user ID / full name.
Definition userinfo.h:36
QDateTime expiry
UserInfo::expiry date/time when key expires.
Definition userinfo.h:62