Add --username option to Clip command. (#3947)

* make Clip accept an attribute name

This allows users to copy arbitrary attributes (e.g. username, notes,
URL) to the clipboard in addition to the password and TOTP values.

* update Clip manpage

* Add findAttributes to CLI utils

* Use case-insensitive search in Show command.

* Use case-insensitive search in Clip command.

Co-authored-by: louib <L0U13@protonmail.com>
This commit is contained in:
James Ring 2020-01-30 12:46:48 -08:00 committed by GitHub
parent 06e0f38523
commit 71a39c37ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 26 deletions

View File

@ -23,7 +23,7 @@ The same password generation options as documented for the generate command can
Analyzes passwords in a database for weaknesses.
.IP "\fBclip\fP [options] <database> <entry> [timeout]"
Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard.
Copies an attribute or the current TOTP (if the \fI-t\fP option is specified) of a database entry to the clipboard. If no attribute name is specified using the \fI-a\fP option, the password is copied. If multiple entries with the same name exist in different groups, only the attribute for the first one is copied. For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard.
.IP "\fBclose\fP"
In interactive mode, closes the currently opened database (see \fIopen\fP).
@ -174,10 +174,14 @@ hour or so).
.SS "Clip options"
.IP "\fB-t\fP, \fB--totp\fP"
Copies the current TOTP instead of current password to clipboard. Will report
an error if no TOTP is configured for the entry.
.IP "\fB-a\fP, \fB--attribute\fP"
Copies the specified attribute to the clipboard. If no attribute is specified,
the password attribute is the default. For example, "\fI-a\fP username" would
copy the username to the clipboard. [Default: password]
.IP "\fB-t\fP, \fB--totp\fP"
Copies the current TOTP instead of the specified attribute to the clipboard.
Will report an error if no TOTP is configured for the entry.
.SS "Create options"

View File

@ -17,7 +17,6 @@
#include <chrono>
#include <cstdlib>
#include <stdio.h>
#include <thread>
#include "Clip.h"
@ -28,14 +27,23 @@
#include "core/Entry.h"
#include "core/Group.h"
const QCommandLineOption Clip::TotpOption = QCommandLineOption(QStringList() << "t"
<< "totp",
QObject::tr("Copy the current TOTP to the clipboard."));
const QCommandLineOption Clip::AttributeOption = QCommandLineOption(
QStringList() << "a"
<< "attribute",
QObject::tr("Copy the given attribute to the clipboard. Defaults to \"password\" if not specified."),
"attr",
"password");
const QCommandLineOption Clip::TotpOption =
QCommandLineOption(QStringList() << "t"
<< "totp",
QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\")."));
Clip::Clip()
{
name = QString("clip");
description = QObject::tr("Copy an entry's password to the clipboard.");
description = QObject::tr("Copy an entry's attribute to the clipboard.");
options.append(Clip::AttributeOption);
options.append(Clip::TotpOption);
positionalArguments.append(
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
@ -51,7 +59,6 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
if (args.size() == 3) {
timeout = args.at(2);
}
bool clipTotp = parser->isSet(Clip::TotpOption);
TextStream errorTextStream(Utils::STDERR);
int timeoutSeconds = 0;
@ -70,16 +77,39 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return EXIT_FAILURE;
}
if (parser->isSet(AttributeOption) && parser->isSet(TotpOption)) {
errorTextStream << QObject::tr("ERROR: Please specify one of --attribute or --totp, not both.") << endl;
return EXIT_FAILURE;
}
QString selectedAttribute = parser->value(AttributeOption);
QString value;
if (clipTotp) {
bool found = false;
if (parser->isSet(TotpOption) || selectedAttribute == "totp") {
if (!entry->hasTotp()) {
errorTextStream << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << endl;
return EXIT_FAILURE;
}
found = true;
value = entry->totp();
} else {
value = entry->password();
QStringList attrs = Utils::findAttributes(*entry->attributes(), selectedAttribute);
if (attrs.size() > 1) {
errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.")
.arg(selectedAttribute, QLocale().createSeparatedList(attrs))
<< endl;
return EXIT_FAILURE;
} else if (attrs.size() == 1) {
found = true;
selectedAttribute = attrs[0];
value = entry->attributes()->value(selectedAttribute);
}
}
if (!found) {
outputTextStream << QObject::tr("Attribute \"%1\" not found.").arg(selectedAttribute) << endl;
return EXIT_FAILURE;
}
int exitCode = Utils::clipText(value);
@ -87,11 +117,7 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
return exitCode;
}
if (clipTotp) {
outputTextStream << QObject::tr("Entry's current TOTP copied to the clipboard!") << endl;
} else {
outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl;
}
outputTextStream << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl;
if (!timeoutSeconds) {
return exitCode;

View File

@ -27,6 +27,7 @@ public:
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
static const QCommandLineOption AttributeOption;
static const QCommandLineOption TotpOption;
};

View File

@ -27,6 +27,8 @@
#include "core/Global.h"
#include "core/Group.h"
#include <QLocale>
const QCommandLineOption Show::TotpOption = QCommandLineOption(QStringList() << "t"
<< "totp",
QObject::tr("Show the entry's current TOTP."));
@ -79,25 +81,33 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
// If no attributes specified, output the default attribute set.
bool showDefaultAttributes = attributes.isEmpty() && !showTotp;
if (attributes.isEmpty() && !showTotp) {
if (showDefaultAttributes) {
attributes = EntryAttributes::DefaultAttributes;
}
// Iterate over the attributes and output them line-by-line.
bool sawUnknownAttribute = false;
bool encounteredError = false;
for (const QString& attributeName : asConst(attributes)) {
if (!entry->attributes()->contains(attributeName)) {
sawUnknownAttribute = true;
QStringList attrs = Utils::findAttributes(*entry->attributes(), attributeName);
if (attrs.isEmpty()) {
encounteredError = true;
errorTextStream << QObject::tr("ERROR: unknown attribute %1.").arg(attributeName) << endl;
continue;
} else if (attrs.size() > 1) {
encounteredError = true;
errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.")
.arg(attributeName, QLocale().createSeparatedList(attrs))
<< endl;
continue;
}
QString canonicalName = attrs[0];
if (showDefaultAttributes) {
outputTextStream << attributeName << ": ";
outputTextStream << canonicalName << ": ";
}
if (entry->attributes()->isProtected(attributeName) && showDefaultAttributes && !showProtectedAttributes) {
if (entry->attributes()->isProtected(canonicalName) && showDefaultAttributes && !showProtectedAttributes) {
outputTextStream << "PROTECTED" << endl;
} else {
outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attributeName)) << endl;
outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(canonicalName)) << endl;
}
}
@ -105,5 +115,5 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
outputTextStream << entry->totp() << endl;
}
return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS;
return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS;
}

View File

@ -331,4 +331,21 @@ namespace Utils
return result;
}
QStringList findAttributes(const EntryAttributes& attributes, const QString& name)
{
QStringList result;
if (attributes.hasKey(name)) {
result.append(name);
return result;
}
for (const QString& key : attributes.keys()) {
if (key.compare(name, Qt::CaseSensitivity::CaseInsensitive) == 0) {
result.append(key);
}
}
return result;
}
} // namespace Utils

View File

@ -20,6 +20,7 @@
#include "cli/TextStream.h"
#include "core/Database.h"
#include "core/EntryAttributes.h"
#include "keys/CompositeKey.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
@ -51,6 +52,14 @@ namespace Utils
QStringList splitCommandString(const QString& command);
/**
* If `attributes` contains an attribute named `name` (case-sensitive),
* returns a list containing only `name`. Otherwise, returns the list of
* all attribute names in `attributes` matching the given name
* (case-insensitive).
*/
QStringList findAttributes(const EntryAttributes& attributes, const QString& name);
namespace Test
{
void setNextPassword(const QString& password);

View File

@ -480,7 +480,7 @@ void TestCli::testClip()
QCOMPARE(clipboard->text(), QString("Password"));
m_stdoutFile->readLine(); // skip prompt line
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's password copied to the clipboard!\n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
// Quiet option
qint64 pos = m_stdoutFile->pos();
@ -491,6 +491,11 @@ void TestCli::testClip()
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(clipboard->text(), QString("Password"));
// Username
Utils::Test::setNextPassword("a");
clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "-a", "username"});
QCOMPARE(clipboard->text(), QString("User Name"));
// TOTP
Utils::Test::setNextPassword("a");
clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"});
@ -538,6 +543,20 @@ void TestCli::testClip()
clipCmd.execute({"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
m_stderrFile->seek(posErr);
QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n"));
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
clipCmd.execute({"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry"});
m_stderrFile->seek(posErr);
QCOMPARE(
m_stderrFile->readAll(),
QByteArray("ERROR: attribute TESTAttribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
clipCmd.execute({"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"});
m_stderrFile->seek(posErr);
QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: Please specify one of --attribute or --totp, not both.\n"));
}
void TestCli::testCreate()
@ -1913,6 +1932,16 @@ void TestCli::testShow()
QByteArray("Sample Entry\n"
"http://www.somesite.com/\n"));
// Test case insensitivity
pos = m_stdoutFile->pos();
Utils::Test::setNextPassword("a");
showCmd.execute({"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
m_stdoutFile->seek(pos);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(),
QByteArray("Sample Entry\n"
"http://www.somesite.com/\n"));
pos = m_stdoutFile->pos();
Utils::Test::setNextPassword("a");
showCmd.execute({"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
@ -1946,6 +1975,19 @@ void TestCli::testShow()
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n"));
// Show with ambiguous attributes
pos = m_stdoutFile->pos();
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
showCmd.execute({"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
m_stdoutFile->seek(pos);
m_stdoutFile->readLine(); // skip password prompt
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(
m_stderrFile->readAll(),
QByteArray("ERROR: attribute Testattribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
}
void TestCli::testInvalidDbFiles()

Binary file not shown.