Merge pull request #347 from H-4ND-H/credential-store

Credential store
This commit is contained in:
Kas 2023-01-30 09:15:32 +01:00 committed by GitHub
commit ee7c780217
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 356 additions and 23 deletions

View File

@ -26,7 +26,6 @@ void Setting::initialize(QMap<Id, QString> &keys) {
keys[Id::TerminalName] = "terminal/name";
keys[Id::TerminalPath] = "terminal/path";
keys[Id::DontTranslate] = "translation/disable";
keys[Id::StoreCredentials] = "credential/store";
keys[Id::AllowSingleInstanceOnly] = "singleInstance";
keys[Id::CheckForUpdatesAutomatically] = "update/check";
keys[Id::InstallUpdatesAutomatically] = "update/download";

View File

@ -50,7 +50,6 @@ public:
TerminalName,
TerminalPath,
DontTranslate,
StoreCredentials,
AllowSingleInstanceOnly,
CheckForUpdatesAutomatically,
InstallUpdatesAutomatically,

View File

@ -2,9 +2,9 @@ if(WIN32)
set(CREDENTIAL_IMPL_FILE WinCred.cpp)
endif()
add_library(cred Cache.cpp CredentialHelper.cpp GitCredential.cpp
add_library(cred Cache.cpp Store.cpp CredentialHelper.cpp GitCredential.cpp
${CREDENTIAL_IMPL_FILE})
target_link_libraries(cred conf Qt5::Core)
target_link_libraries(cred conf git Qt5::Core)
set_target_properties(cred PROPERTIES AUTOMOC ON)

View File

@ -11,7 +11,9 @@
#include "Cache.h"
#include "GitCredential.h"
#include "WinCred.h"
#include "Store.h"
#include "conf/Settings.h"
#include "git/Config.h"
#include <QLibrary>
#include <QPointer>
#include <QSettings>
@ -22,27 +24,36 @@ namespace {
const QString kLogKey = "credential/log";
const QString cacheStoreName = "cache";
const QString storeStoreName = "store";
const QString osxKeyChainStoreName = "osxkeychain";
const QString winCredStoreName = "wincred";
const QString libSecretStoreName = "libsecret";
const QString gnomeKeyringStoreName = "gnome-keyring";
} // namespace
CredentialHelper *CredentialHelper::instance() {
static QPointer<CredentialHelper> instance;
if (!instance) {
if (Settings::instance()->value(Setting::Id::StoreCredentials).toBool()) {
#if defined(Q_OS_MAC)
instance = new GitCredential("osxkeychain");
#elif defined(Q_OS_WIN)
// The git wincred helper fails for some users.
instance = new WinCred;
#else
QLibrary lib("secret-1", 0);
if (lib.load()) {
instance = new GitCredential("libsecret");
} else {
QLibrary lib("gnome-keyring", 0);
if (lib.load())
instance = new GitCredential("gnome-keyring");
git::Config config = git::Config::global();
auto helperName = config.value<QString>("credential.helper");
if (isHelperValid(helperName)) {
if (helperName == cacheStoreName) {
instance = new Cache;
} else if (helperName == storeStoreName) {
auto path =
QString::fromLocal8Bit(qgetenv("HOME") + "/.git-credentials");
instance = new Store(path);
}
#if defined(Q_OS_WIN)
else if (helperName == winCredStoreName) {
instance = new WinCred;
}
#endif
else {
instance = new GitCredential(helperName);
}
}
if (!instance)
@ -52,6 +63,31 @@ CredentialHelper *CredentialHelper::instance() {
return instance;
}
bool CredentialHelper::isHelperValid(const QString &name) {
return !name.isEmpty();
}
QStringList CredentialHelper::getAvailableHelperNames() {
QStringList list;
list.append(cacheStoreName);
list.append(storeStoreName);
#if defined(Q_OS_MAC)
list.append(osxKeyChainStoreName);
#elif defined(Q_OS_WIN)
list.append(winCredStoreName);
#else
QLibrary lib("secret-1", 0);
if (lib.load()) {
list.append(libSecretStoreName);
}
QLibrary lib2(gnomeKeyringStoreName, 0);
if (lib2.load()) {
list.append(gnomeKeyringStoreName);
}
#endif
return list;
}
bool CredentialHelper::isLoggingEnabled() {
return QSettings().value(kLogKey).toBool();
}

View File

@ -33,6 +33,9 @@ public:
static bool isLoggingEnabled();
static void setLoggingEnabled(bool enabled);
static QStringList getAvailableHelperNames();
static bool isHelperValid(const QString &name);
protected:
static void log(const QString &text);
};

111
src/cred/Store.cpp Normal file
View File

@ -0,0 +1,111 @@
//
// Copyright (c) 2022, Gittyup Community
//
// This software is licensed under the MIT License. The LICENSE.md file
// describes the conditions under which this software may be distributed.
//
// Author: Hessamoddin Hediehloo(H-4ND-H)
//
#include "Store.h"
#include <QUrl>
#include <QFile>
#include <QTextStream>
namespace {
QString host(const QString &url) {
QString host = QUrl(url).host();
if (!host.isEmpty())
return host;
// Extract hostname from SSH URL.
int end = url.indexOf(':');
int begin = url.indexOf('@') + 1;
return url.mid(begin, end - begin);
}
QString protocol(const QString &url) {
QString scheme = QUrl(url).scheme();
return !scheme.isEmpty() ? scheme : "ssh";
}
} // namespace
Store::Store(const QString &path) { mPath = path; }
QMap<QString, QMap<QString, QMap<QString, QString>>> Store::readCredFile() {
QMap<QString, QMap<QString, QMap<QString, QString>>> store;
QFile file(mPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return store;
while (!file.atEnd()) {
auto line = file.readLine();
auto urlStr = QUrl::fromPercentEncoding(line);
auto urlObj = QUrl::fromUserInput(urlStr);
store[urlObj.scheme()][urlObj.host()][urlObj.userName()] =
urlObj.password();
}
file.close();
return store;
}
bool Store::extractUserPass(const QMap<QString, QString> &map,
QString &username, QString &password) {
if (map.isEmpty())
return false;
if (username.isEmpty())
username = map.keys().first();
if (!map.contains(username))
return false;
password = map.value(username);
return !username.isEmpty() && !password.isEmpty();
}
bool Store::get(const QString &url, QString &username, QString &password) {
auto store = readCredFile();
const QMap<QString, QString> &map = store[protocol(url)][host(url)];
return extractUserPass(map, username, password);
}
bool Store::store(const QString &url, const QString &username,
const QString &password) {
auto store = readCredFile();
store[protocol(url)][host(url)][username] = password;
QFile file(mPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
return false;
foreach (const auto &protocolKey, store.keys()) {
auto protocol = store[protocolKey];
foreach (const auto &hostKey, protocol.keys()) {
auto host = protocol[hostKey];
foreach (const auto &usernameKey, host.keys()) {
QUrl temp;
temp.setScheme(protocolKey);
temp.setHost(hostKey);
temp.setUserName(usernameKey);
temp.setPassword(host[usernameKey]);
auto encoded = QUrl::toPercentEncoding(temp.toString(), "@:/");
QTextStream fout(&file);
fout << encoded << "\n";
}
}
}
file.close();
return true;
}
QString Store::command() const { return ""; }

34
src/cred/Store.h Normal file
View File

@ -0,0 +1,34 @@
//
// Copyright (c) 2022, Gittyup Community
//
// This software is licensed under the MIT License. The LICENSE.md file
// describes the conditions under which this software may be distributed.
//
// Author: Hessamoddin Hediehloo(H-4ND-H)
//
#ifndef STORE_H
#define STORE_H
#include "CredentialHelper.h"
#include <QMap>
class Store : public CredentialHelper {
public:
Store(const QString &path);
bool get(const QString &url, QString &username, QString &password) override;
bool store(const QString &url, const QString &username,
const QString &password) override;
private:
QString command() const;
QMap<QString, QMap<QString, QMap<QString, QString>>> readCredFile();
bool extractUserPass(const QMap<QString, QString> &map, QString &username,
QString &password);
QString mPath;
};
#endif

View File

@ -109,6 +109,8 @@ public:
mStoreCredentials =
new QCheckBox(tr("Store credentials in secure storage"), this);
mAvailableStores = new QComboBox(this);
QLabel *privacy = new QLabel(tr("<a href='view'>View privacy policy</a>"));
connect(privacy, &QLabel::linkActivated,
[] { AboutDialog::openSharedInstance(AboutDialog::Privacy); });
@ -122,6 +124,7 @@ public:
form->addRow(QString(), mAutoPrune);
form->addRow(tr("Language:"), mNoTranslation);
form->addRow(tr("Credentials:"), mStoreCredentials);
form->addRow(tr("Credential store type:"), mAvailableStores);
form->addRow(QString(), privacy);
mSingleInstance =
@ -181,11 +184,27 @@ public:
Settings::instance()->setValue(Setting::Id::DontTranslate, checked);
});
connect(mStoreCredentials, &QCheckBox::toggled, [](bool checked) {
Settings::instance()->setValue(Setting::Id::StoreCredentials, checked);
connect(mStoreCredentials, &QCheckBox::toggled, [this](bool checked) {
git::Config config = git::Config::global();
mAvailableStores->setEnabled(checked);
if (checked) {
auto store = mAvailableStores->currentText();
config.setValue("credential.helper", store);
} else {
config.remove("credential.helper");
}
delete CredentialHelper::instance();
});
connect(mAvailableStores, &QComboBox::currentTextChanged,
[](const QString &text) {
git::Config config = git::Config::global();
config.setValue("credential.helper", text);
delete CredentialHelper::instance();
});
connect(mSingleInstance, &QCheckBox::toggled, [](bool checked) {
Settings::instance()->setValue(Setting::Id::AllowSingleInstanceOnly,
checked);
@ -198,6 +217,7 @@ public:
mEmail->setText(config.value<QString>("user.email"));
Settings *settings = Settings::instance();
mFetch->setChecked(
settings->value(Setting::Id::FetchAutomatically).toBool());
mFetchMinutes->setValue(
@ -213,8 +233,17 @@ public:
mNoTranslation->setChecked(
settings->value(Setting::Id::DontTranslate).toBool());
mStoreCredentials->setChecked(
settings->value(Setting::Id::StoreCredentials).toBool());
auto currentHelper = config.value<QString>("credential.helper");
auto checked = CredentialHelper::isHelperValid(currentHelper);
mStoreCredentials->setChecked(checked);
auto availableHelpers = CredentialHelper::getAvailableHelperNames();
foreach (auto helper, availableHelpers) {
mAvailableStores->addItem(helper);
}
mAvailableStores->setCurrentText(currentHelper);
mAvailableStores->setEditable(true);
mSingleInstance->setChecked(
settings->value(Setting::Id::AllowSingleInstanceOnly).toBool());
@ -231,6 +260,7 @@ private:
QCheckBox *mAutoPrune;
QCheckBox *mNoTranslation;
QCheckBox *mStoreCredentials;
QComboBox *mAvailableStores;
QCheckBox *mSingleInstance;
};

View File

@ -109,6 +109,7 @@ test(NAME Submodule)
test(NAME referencelist)
test(NAME amend)
test(NAME SshConfig)
test(NAME store)
test(NAME fileContextMenu NO_WIN32_OFFSCREEN)
test(NAME Setting)
test(NAME commitMessageTemplate)

View File

@ -10,7 +10,7 @@
#include "Test.h"
#include "git/Config.h"
#include "ui/RepoView.h"
//#include <JlCompress.h>
// #include <JlCompress.h>
#include <exception>
#include <QFileInfo>
#include "zip.h"

120
test/store.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "Test.h"
#include "cred/Store.h"
class TestStore : public QObject {
Q_OBJECT
private:
QTemporaryDir mTempDir;
QString getTestFilePath();
private slots:
void initTestCase();
void readUserPassTestCase();
void wrongProtocolTestCase();
void wrongUrlTestCase();
void wrongUsernameTestCase();
void saveUserPassTestCase();
void wrongFilePathTestCase();
void cleanupTestCase();
};
QString TestStore::getTestFilePath() {
return mTempDir.path() + "/fakeCred.txt";
}
void TestStore::initTestCase() {
QFile file(getTestFilePath());
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
QTextStream fout(&file);
fout << "http://joneDoe:secretPassword@192.168.1.1\n";
fout << "https://janeDoe:securePassword@192.168.2.2:3000\n";
file.close();
}
}
void TestStore::readUserPassTestCase() {
Store store(getTestFilePath());
auto url = "https://192.168.2.2:3000/h-4nd-h/fake.git";
QString username;
QString password;
auto result = store.get(url, username, password);
QVERIFY(result);
QCOMPARE(username, "janeDoe");
QCOMPARE(password, "securePassword");
}
void TestStore::wrongProtocolTestCase() {
Store store(getTestFilePath());
auto url = "https://192.168.1.1/h-4nd-h/fake.git";
QString username;
QString password;
auto result = store.get(url, username, password);
QVERIFY(!result);
}
void TestStore::wrongUrlTestCase() {
Store store(getTestFilePath());
auto url = "http://192.168.1.2/h-4nd-h/fake.git";
QString username;
QString password;
auto result = store.get(url, username, password);
QVERIFY(!result);
}
void TestStore::wrongUsernameTestCase() {
Store store(getTestFilePath());
auto url = "http://192.168.1.1/h-4nd-h/fake.git";
QString username = "janeDoe";
QString password;
auto result = store.get(url, username, password);
QVERIFY(!result);
}
void TestStore::saveUserPassTestCase() {
Store store(getTestFilePath());
auto url = "http://192.168.1.2/h-4nd-h/fake.git";
QString username = "NewUser";
QString password = "NewPassword";
auto result = store.store(url, username, password);
QVERIFY(result);
QString readBackUsername;
QString readBackpassword;
auto result2 = store.get(url, readBackUsername, readBackpassword);
QVERIFY(result2);
QCOMPARE(readBackUsername, "NewUser");
QCOMPARE(readBackpassword, "NewPassword");
}
void TestStore::wrongFilePathTestCase() {
Store store("veryWrongPath");
auto url = "http://192.168.1.2/h-4nd-h/fake.git";
QString username = "";
QString password = "";
auto result = store.get(url, username, password);
QVERIFY(!result);
}
void TestStore::cleanupTestCase() {
QFile file(getTestFilePath());
file.remove();
}
TEST_MAIN(TestStore)
#include "store.moc"