mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
SSH Agent: Split private key selection
This commit is contained in:
parent
d2a59c556e
commit
76e6d498cf
@ -47,6 +47,7 @@
|
||||
#include "gui/FileDialog.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "gui/Clipboard.h"
|
||||
#include "gui/Font.h"
|
||||
#include "gui/entry/AutoTypeAssociationsModel.h"
|
||||
#include "gui/entry/EntryAttachmentsModel.h"
|
||||
#include "gui/entry/EntryAttributesModel.h"
|
||||
@ -267,7 +268,15 @@ void EditEntryWidget::setupSSHAgent()
|
||||
{
|
||||
m_sshAgentUi->setupUi(m_sshAgentWidget);
|
||||
|
||||
connect(m_sshAgentUi->privateKeyComboBox, SIGNAL(currentTextChanged(QString)), SLOT(updateSSHAgentKeyInfo()));
|
||||
QFont fixedFont = Font::fixedFont();
|
||||
m_sshAgentUi->fingerprintTextLabel->setFont(fixedFont);
|
||||
m_sshAgentUi->commentTextLabel->setFont(fixedFont);
|
||||
m_sshAgentUi->publicKeyEdit->setFont(fixedFont);
|
||||
|
||||
connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(clicked(bool)), SLOT(updateSSHAgentKeyInfo()));
|
||||
connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), SLOT(updateSSHAgentKeyInfo()));
|
||||
connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(clicked(bool)), SLOT(updateSSHAgentKeyInfo()));
|
||||
connect(m_sshAgentUi->externalFileEdit, SIGNAL(textChanged(QString)), SLOT(updateSSHAgentKeyInfo()));
|
||||
connect(m_sshAgentUi->browseButton, SIGNAL(clicked()), SLOT(browsePrivateKey()));
|
||||
connect(m_sshAgentUi->addToAgentButton, SIGNAL(clicked()), SLOT(addKeyToAgent()));
|
||||
connect(m_sshAgentUi->removeFromAgentButton, SIGNAL(clicked()), SLOT(removeKeyFromAgent()));
|
||||
@ -279,8 +288,6 @@ void EditEntryWidget::setupSSHAgent()
|
||||
|
||||
void EditEntryWidget::updateSSHAgent()
|
||||
{
|
||||
// TODO: unsafe use of translations
|
||||
QString prefix = tr("Attachment") + ": ";
|
||||
KeeAgentSettings settings;
|
||||
settings.fromXml(m_entryAttachments->value("KeeAgent.settings"));
|
||||
|
||||
@ -289,29 +296,33 @@ void EditEntryWidget::updateSSHAgent()
|
||||
m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(settings.useConfirmConstraintWhenAdding());
|
||||
m_sshAgentUi->lifetimeCheckBox->setChecked(settings.useLifetimeConstraintWhenAdding());
|
||||
m_sshAgentUi->lifetimeSpinBox->setValue(settings.lifetimeConstraintDuration());
|
||||
m_sshAgentUi->privateKeyComboBox->clear();
|
||||
m_sshAgentUi->attachmentComboBox->clear();
|
||||
m_sshAgentUi->addToAgentButton->setEnabled(false);
|
||||
m_sshAgentUi->removeFromAgentButton->setEnabled(false);
|
||||
m_sshAgentUi->copyToClipboardButton->setEnabled(false);
|
||||
|
||||
m_sshAgentUi->attachmentComboBox->addItem("");
|
||||
|
||||
for (QString fileName : m_entryAttachments->keys()) {
|
||||
if (fileName == "KeeAgent.settings") {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_sshAgentUi->privateKeyComboBox->addItem(prefix + fileName);
|
||||
m_sshAgentUi->attachmentComboBox->addItem(fileName);
|
||||
}
|
||||
|
||||
m_sshAgentUi->attachmentComboBox->setCurrentText(settings.attachmentName());
|
||||
m_sshAgentUi->externalFileEdit->setText(settings.fileName());
|
||||
|
||||
if (settings.selectedType() == "attachment") {
|
||||
m_sshAgentUi->privateKeyComboBox->setCurrentText(prefix + settings.attachmentName());
|
||||
} else if (!settings.fileName().isEmpty()) {
|
||||
m_sshAgentUi->privateKeyComboBox->addItem(settings.fileName());
|
||||
m_sshAgentUi->privateKeyComboBox->setCurrentText(settings.fileName());
|
||||
m_sshAgentUi->attachmentRadioButton->setChecked(true);
|
||||
} else {
|
||||
m_sshAgentUi->privateKeyComboBox->setCurrentText("");
|
||||
m_sshAgentUi->externalFileRadioButton->setChecked(true);
|
||||
}
|
||||
|
||||
m_sshAgentSettings = settings;
|
||||
|
||||
updateSSHAgentKeyInfo();
|
||||
}
|
||||
|
||||
void EditEntryWidget::updateSSHAgentKeyInfo()
|
||||
@ -319,28 +330,24 @@ void EditEntryWidget::updateSSHAgentKeyInfo()
|
||||
m_sshAgentUi->addToAgentButton->setEnabled(false);
|
||||
m_sshAgentUi->removeFromAgentButton->setEnabled(false);
|
||||
m_sshAgentUi->copyToClipboardButton->setEnabled(false);
|
||||
m_sshAgentUi->fingerprintEdit->setText("");
|
||||
m_sshAgentUi->commentEdit->setText("");
|
||||
m_sshAgentUi->fingerprintTextLabel->setText(tr("n/a"));
|
||||
m_sshAgentUi->commentTextLabel->setText(tr("n/a"));
|
||||
m_sshAgentUi->decryptButton->setEnabled(false);
|
||||
m_sshAgentUi->publicKeyEdit->document()->setPlainText("");
|
||||
|
||||
if (m_sshAgentUi->privateKeyComboBox->currentText().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenSSHKey key;
|
||||
|
||||
if (!getOpenSSHKey(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sshAgentUi->fingerprintEdit->setText(key.fingerprint());
|
||||
m_sshAgentUi->fingerprintTextLabel->setText(key.fingerprint());
|
||||
|
||||
if (key.encrypted()) {
|
||||
m_sshAgentUi->commentEdit->setText(tr("(encrypted)"));
|
||||
m_sshAgentUi->commentTextLabel->setText(tr("(encrypted)"));
|
||||
m_sshAgentUi->decryptButton->setEnabled(true);
|
||||
} else {
|
||||
m_sshAgentUi->commentEdit->setText(key.comment());
|
||||
m_sshAgentUi->commentTextLabel->setText(key.comment());
|
||||
}
|
||||
|
||||
m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey());
|
||||
@ -357,7 +364,7 @@ void EditEntryWidget::updateSSHAgentKeyInfo()
|
||||
void EditEntryWidget::saveSSHAgentConfig()
|
||||
{
|
||||
KeeAgentSettings settings;
|
||||
QString privateKeyPath = m_sshAgentUi->privateKeyComboBox->currentText();
|
||||
QString privateKeyPath = m_sshAgentUi->attachmentComboBox->currentText();
|
||||
|
||||
settings.setAddAtDatabaseOpen(m_sshAgentUi->addKeyToAgentCheckBox->isChecked());
|
||||
settings.setRemoveAtDatabaseClose(m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked());
|
||||
@ -365,17 +372,13 @@ void EditEntryWidget::saveSSHAgentConfig()
|
||||
settings.setUseLifetimeConstraintWhenAdding(m_sshAgentUi->lifetimeCheckBox->isChecked());
|
||||
settings.setLifetimeConstraintDuration(m_sshAgentUi->lifetimeSpinBox->value());
|
||||
|
||||
// TODO: unsafe use of translations
|
||||
QString prefix = tr("Attachment") + ": ";
|
||||
if (privateKeyPath.startsWith(prefix)) {
|
||||
if (m_sshAgentUi->attachmentRadioButton->isChecked()) {
|
||||
settings.setSelectedType("attachment");
|
||||
settings.setAttachmentName(privateKeyPath.remove(0, prefix.length()));
|
||||
settings.setFileName("");
|
||||
} else {
|
||||
settings.setSelectedType("file");
|
||||
settings.setFileName(privateKeyPath);
|
||||
settings.setAttachmentName("");
|
||||
}
|
||||
settings.setAttachmentName(m_sshAgentUi->attachmentComboBox->currentText());
|
||||
settings.setFileName(m_sshAgentUi->externalFileEdit->text());
|
||||
|
||||
// we don't use this as we don't run an agent but for compatibility we set it if necessary
|
||||
settings.setAllowUseOfSshKey(settings.addAtDatabaseOpen() || settings.removeAtDatabaseClose());
|
||||
@ -396,23 +399,22 @@ void EditEntryWidget::browsePrivateKey()
|
||||
{
|
||||
QString fileName = QFileDialog::getOpenFileName(this, tr("Select private key"), "");
|
||||
if (!fileName.isEmpty()) {
|
||||
m_sshAgentUi->privateKeyComboBox->addItem(fileName);
|
||||
m_sshAgentUi->privateKeyComboBox->setCurrentText(fileName);
|
||||
m_sshAgentUi->externalFileEdit->setText(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key)
|
||||
{
|
||||
QString privateKeyPath = m_sshAgentUi->privateKeyComboBox->currentText();
|
||||
QByteArray privateKeyData;
|
||||
|
||||
// TODO: unsafe use of translations
|
||||
QString prefix = tr("Attachment") + ": ";
|
||||
if (privateKeyPath.startsWith(prefix)) {
|
||||
QString attachmentName = privateKeyPath.remove(0, prefix.length());
|
||||
privateKeyData = m_entryAttachments->value(attachmentName);
|
||||
if (m_sshAgentUi->attachmentRadioButton->isChecked()) {
|
||||
privateKeyData = m_entryAttachments->value(m_sshAgentUi->attachmentComboBox->currentText());
|
||||
} else {
|
||||
QFile localFile(privateKeyPath);
|
||||
QFile localFile(m_sshAgentUi->externalFileEdit->text());
|
||||
|
||||
if (localFile.fileName().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localFile.size() > 1024 * 1024) {
|
||||
showMessage(tr("File too large to be a private key"), MessageWidget::Error);
|
||||
@ -427,6 +429,10 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key)
|
||||
privateKeyData = localFile.readAll();
|
||||
}
|
||||
|
||||
if (privateKeyData.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!key.parse(privateKeyData)) {
|
||||
showMessage(key.errorString(), MessageWidget::Error);
|
||||
return false;
|
||||
@ -446,7 +452,7 @@ void EditEntryWidget::addKeyToAgent()
|
||||
if (!key.openPrivateKey(m_entry->password())) {
|
||||
showMessage(key.errorString(), MessageWidget::Error);
|
||||
} else {
|
||||
m_sshAgentUi->commentEdit->setText(key.comment());
|
||||
m_sshAgentUi->commentTextLabel->setText(key.comment());
|
||||
m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey());
|
||||
}
|
||||
|
||||
@ -484,7 +490,7 @@ void EditEntryWidget::decryptPrivateKey()
|
||||
if (!key.openPrivateKey(m_entry->password())) {
|
||||
showMessage(key.errorString(), MessageWidget::Error);
|
||||
} else {
|
||||
m_sshAgentUi->commentEdit->setText(key.comment());
|
||||
m_sshAgentUi->commentTextLabel->setText(key.comment());
|
||||
m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey());
|
||||
}
|
||||
}
|
||||
|
@ -26,63 +26,6 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="1" colspan="4">
|
||||
<widget class="QCheckBox" name="removeKeyFromAgentCheckBox">
|
||||
<property name="text">
|
||||
<string>Remove key from agent when database is closed/locked</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLabel" name="fingerprintLabel">
|
||||
<property name="text">
|
||||
<string>Fingerprint</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="3" colspan="2">
|
||||
<widget class="QPlainTextEdit" name="publicKeyEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="3" colspan="2">
|
||||
<widget class="QLineEdit" name="fingerprintEdit">
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="privateKeyLabel">
|
||||
<property name="text">
|
||||
<string>Private key</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLabel" name="publicKeyLabel">
|
||||
<property name="text">
|
||||
<string>Public key</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="4">
|
||||
<layout class="QHBoxLayout" name="removeKeyLayout">
|
||||
<item>
|
||||
@ -118,15 +61,32 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="QLabel" name="commentLabel">
|
||||
<widget class="QLabel" name="fingerprintLabel">
|
||||
<property name="text">
|
||||
<string>Comment</string>
|
||||
<string>Fingerprint</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="4">
|
||||
<widget class="QCheckBox" name="removeKeyFromAgentCheckBox">
|
||||
<property name="text">
|
||||
<string>Remove key from agent when database is closed/locked</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<widget class="QLabel" name="publicKeyLabel">
|
||||
<property name="text">
|
||||
<string>Public key</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="4">
|
||||
<widget class="QCheckBox" name="addKeyToAgentCheckBox">
|
||||
<property name="text">
|
||||
@ -134,26 +94,129 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QComboBox" name="privateKeyComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLabel" name="commentLabel">
|
||||
<property name="text">
|
||||
<string>Comment</string>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="4">
|
||||
<widget class="QPushButton" name="browseButton">
|
||||
<item row="12" column="4">
|
||||
<widget class="QPushButton" name="decryptButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
<string>Decrypt</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="3" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="fingerprintTextLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>n/a</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="14" column="3" colspan="2">
|
||||
<widget class="QPushButton" name="copyToClipboardButton">
|
||||
<property name="text">
|
||||
<string>Copy to clipboard</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="4">
|
||||
<widget class="QGroupBox" name="privateKeyGroupBox">
|
||||
<property name="title">
|
||||
<string>Private key</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="3" column="0">
|
||||
<widget class="QRadioButton" name="externalFileRadioButton">
|
||||
<property name="text">
|
||||
<string>External file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QPushButton" name="browseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="attachmentRadioButton">
|
||||
<property name="text">
|
||||
<string>Attachment</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="externalFileEdit"/>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="agentActionsLayout" stretch="0,0">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addToAgentButton">
|
||||
<property name="text">
|
||||
<string>Add to agent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeFromAgentButton">
|
||||
<property name="text">
|
||||
<string>Remove from agent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="attachmentComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="4">
|
||||
<widget class="QCheckBox" name="requireUserConfirmationCheckBox">
|
||||
<property name="text">
|
||||
@ -162,43 +225,48 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="3" colspan="2">
|
||||
<widget class="QPushButton" name="copyToClipboardButton">
|
||||
<property name="text">
|
||||
<string>Copy to clipboard</string>
|
||||
<widget class="QPlainTextEdit" name="publicKeyEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="3">
|
||||
<layout class="QHBoxLayout" name="agentActionsLayout" stretch="0,0">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addToAgentButton">
|
||||
<property name="text">
|
||||
<string>Add to agent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeFromAgentButton">
|
||||
<property name="text">
|
||||
<string>Remove from agent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="11" column="3">
|
||||
<widget class="QLineEdit" name="commentEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="4">
|
||||
<widget class="QPushButton" name="decryptButton">
|
||||
<property name="text">
|
||||
<string>Decrypt</string>
|
||||
</property>
|
||||
</widget>
|
||||
<item row="12" column="3">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="commentTextLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>n/a</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
Loading…
Reference in New Issue
Block a user