Store connection configuration as key value pairs

Add migration for the sqlite database.
Because the Qt SQL library is a bit hard to work with use sqlite through custom wrapper.
This commit is contained in:
eelke 2025-02-22 19:59:24 +01:00
parent 4caccf1000
commit aac55b0ed1
17 changed files with 276439 additions and 384 deletions

View file

@ -2,11 +2,10 @@
#include "MasterController.h"
#include "ConnectionManagerWindow.h"
#include "ConnectionListModel.h"
#include "PasswordManager.h"
#include "utils/PasswordManager.h"
#include "DatabaseWindow.h"
#include "BackupDialog.h"
#include "PasswordPromptDialog.h"
#include "ScopeGuard.h"
#include "ConnectionConfigurationWidget.h"
#include <QSqlQuery>
#include <QInputDialog>
@ -133,11 +132,14 @@ void ConnectionController::addGroup()
auto result = QInputDialog::getText(nullptr, tr("Add new connection group"),
tr("Group name"));
if (!result.isEmpty()) {
auto res = m_connectionTreeModel->addGroup(result);
if (std::holds_alternative<QSqlError>(res)) {
try
{
m_connectionTreeModel->addGroup(result);
}
catch (const SQLiteException &ex) {
QMessageBox::critical(nullptr, tr("Add group failed"),
tr("Failed to add group.\n") +
std::get<QSqlError>(res).text());
QString(ex.what()));
}
}
}
@ -236,19 +238,11 @@ bool ConnectionController::decodeConnectionPassword(QUuid id, QByteArray encoded
void ConnectionController::resetPasswordManager()
{
auto&& user_cfg_db = m_masterController->userConfigDatabase();
user_cfg_db.transaction();
try
{
m_passwordManager->resetMasterPassword(user_cfg_db);
m_connectionTreeModel->clearAllPasswords();
user_cfg_db.commit();
}
catch (...)
{
user_cfg_db.rollback();
throw;
}
SQLiteConnection& user_cfg_db = m_masterController->userConfigDatabase();
SQLiteTransaction tx(user_cfg_db);
m_passwordManager->resetMasterPassword(user_cfg_db);
m_connectionTreeModel->clearAllPasswords();
tx.Commit();
}
bool ConnectionController::UnlockPasswordManagerIfNeeded()

View file

@ -56,19 +56,12 @@ SELECT migration_id
FROM _migration;)__";
const char * const q_insert_or_replace_into_connection =
R"__(INSERT OR REPLACE INTO connection
VALUES (:uuid, :name, :conngroup_id, :host, :hostaddr, :port, :user, :dbname,
:sslmode, :sslcert, :sslkey, :sslrootcert, :sslcrl, :password);
)__" ;
// Keeping migration function name and id DRY
#define APPLY_MIGRATION(id) ApplyMigration(#id, &MigrationDirector::id)
class MigrationDirector {
public:
explicit MigrationDirector(QSqlDatabase &db)
explicit MigrationDirector(SQLiteConnection &db)
: db(db)
{
}
@ -81,12 +74,12 @@ R"__(INSERT OR REPLACE INTO connection
}
private:
QSqlDatabase &db;
SQLiteConnection &db;
std::unordered_set<QString> present;
void M20250215_0933_Parameters()
{
Exec(R"__(
db.Exec(R"__(
CREATE TABLE connection_parameter (
connection_uuid TEXT,
pname TEXT,
@ -95,16 +88,29 @@ CREATE TABLE connection_parameter (
PRIMARY KEY(connection_uuid, pname)
);)__");
db.Exec(R"__(
INSERT INTO connection_parameter (connection_uuid, pname, pvalue)
SELECT uuid, 'sslmode' AS pname, CASE
WHEN sslmode = '0' THEN 'disable'
WHEN sslmode = '1' THEN 'allow'
WHEN sslmode = '2' THEN 'prefer'
WHEN sslmode = '3' THEN 'require'
WHEN sslmode = '4' THEN 'verify_ca'
WHEN sslmode = '5' THEN 'verify_full'
END AS pvalue
FROM connection
WHERE sslmode is not null and sslmode between 0 and 5)__");
for (QString key : { "host", "hostaddr", "user", "dbname", "sslmode", "sslcert", "sslkey", "sslrootcert", "sslcrl" })
for (QString key : { "host", "hostaddr", "user", "dbname", "sslcert", "sslkey", "sslrootcert", "sslcrl" })
{
Exec(
db.Exec(
"INSERT INTO connection_parameter (connection_uuid, pname, pvalue)"
" SELECT uuid, '" % key % "', " % key % "\n"
" FROM connection\n"
" WHERE " % key % " IS NOT NULL and " % key % " <> '';");
" FROM connection\n"
" WHERE " % key % " IS NOT NULL and " % key % " <> '';");
}
Exec(R"__(
db.Exec(R"__(
INSERT INTO connection_parameter (connection_uuid, pname, pvalue)
SELECT uuid, 'port', port
FROM connection
@ -114,118 +120,76 @@ INSERT INTO connection_parameter (connection_uuid, pname, pvalue)
for (QString column : { "host", "hostaddr", "user", "dbname", "sslmode", "sslcert", "sslkey", "sslrootcert", "sslcrl", "port" })
{
// sqlite does not seem to support dropping more then one column per alter table
Exec("ALTER TABLE connection DROP COLUMN " % column % ";");
db.Exec("ALTER TABLE connection DROP COLUMN " % column % ";");
}
}
void Exec(QString query)
{
QSqlQuery q(query, db);
Verify(q);
}
void ApplyMigration(QString migration_id, void (MigrationDirector::*func)())
{
if (!present.contains(migration_id))
{
if (!db.transaction())
{
throw std::runtime_error("Failed to start transaction on user configuration database");
}
SQLiteTransaction tx(db);
(this->*func)();
RegisterMigration(migration_id);
if (!db.commit())
{
db.rollback();
throw std::runtime_error("Failed to commit transaction on user configuration database");
}
tx.Commit();
}
}
std::unordered_set<QString> LoadMigrations()
{
std::unordered_set<QString> result;
QSqlQuery q(q_load_migrations_present, db);
Verify(q);
while (q.next())
auto stmt = db.Prepare(q_load_migrations_present);
while (stmt.Step())
{
result.insert(q.value(0).toString());
result.insert(stmt.ColumnText(0));
}
return result;
}
void RegisterMigration(QString migrationId)
{
const char * const q_register_migration =
R"__(INSERT INTO _migration VALUES (:id);)__" ;
QSqlQuery q(db);
q.prepare(q_register_migration);
q.bindValue(":id", migrationId);
if (!q.exec()) {
Verify(q);
}
}
void Verify(QSqlQuery &q)
{
auto err = q.lastError();
if (err.type() == QSqlError::NoError)
return;
db.rollback();
QString errString = err.text();
throw std::runtime_error(errString.toStdString());
auto stmt = db.Prepare("INSERT INTO _migration VALUES (?1);");
stmt.Bind(1, migrationId);
stmt.Step();
}
void InitConnectionTables()
{
// Original schema
QSqlQuery q_create_table(db);
q_create_table.exec(q_create_table_conngroup);
Verify(q_create_table);
q_create_table.exec(q_create_table_connection);
Verify(q_create_table);
db.Exec(q_create_table_conngroup);
db.Exec(q_create_table_connection);
// Start using migrations
q_create_table.exec(q_create_table_migrations);
Verify(q_create_table);
db.Exec(q_create_table_migrations);
}
};
std::optional<QSqlError> SaveConnectionConfig(QSqlDatabase &db, const ConnectionConfig &cc, int conngroup_id)
void SaveConnectionConfig(SQLiteConnection &db, const ConnectionConfig &cc, int conngroup_id)
{
QSqlQuery q(db);
q.prepare(q_insert_or_replace_into_connection);
q.bindValue(":uuid", cc.uuid().toString());
q.bindValue(":name", cc.name());
q.bindValue(":conngroup_id", conngroup_id);
q.bindValue(":host", cc.host());
q.bindValue(":hostaddr", cc.hostAddr());
q.bindValue(":port", (int)cc.port());
q.bindValue(":user", cc.user());
q.bindValue(":dbname", cc.dbname());
q.bindValue(":sslmode", static_cast<int>(cc.sslMode()));
q.bindValue(":sslcert", cc.sslCert());
q.bindValue(":sslkey", cc.sslKey());
q.bindValue(":sslrootcert", cc.sslRootCert());
q.bindValue(":sslcrl", cc.sslCrl());
auto& encodedPassword = cc.encodedPassword();
if (encodedPassword.isEmpty())
q.bindValue(":password", QVariant());
else
q.bindValue(":password", encodedPassword);
const char * const q_insert_or_replace_into_connection =
R"__(INSERT OR REPLACE INTO connection
VALUES (?1, ?2, ?3, ?4);
)__" ;
if (!q.exec()) {
auto sql_error = q.lastError();
return { sql_error };
}
return {};
QByteArray b64; // needs to stay in scope until query is executed
SQLiteTransaction tx(db);
SQLitePreparedStatement stmt = db.Prepare(q_insert_or_replace_into_connection);
stmt.Bind(1, cc.uuid().toString());
stmt.Bind(2, cc.name());
stmt.Bind(3, conngroup_id);
auto& encodedPassword = cc.encodedPassword();
if (!encodedPassword.isEmpty())
{
b64 = encodedPassword.toBase64(QByteArray::Base64Encoding);
stmt.Bind(4, b64.data(), b64.length());
}
stmt.Step();
}
} // end of unnamed namespace
ConnectionTreeModel::ConnectionTreeModel(QObject *parent, QSqlDatabase &db)
ConnectionTreeModel::ConnectionTreeModel(QObject *parent, SQLiteConnection &db)
: QAbstractItemModel(parent)
, m_db(db)
{
@ -237,56 +201,61 @@ void ConnectionTreeModel::load()
MigrationDirector md(m_db);
md.Execute();
QSqlQuery q(m_db);
q.prepare("SELECT conngroup_id, gname FROM conngroup;");
if (!q.exec()) {
// auto err = q_create_table.lastError();
// return { false, err };
throw std::runtime_error("Loading groups failed");
}
while (q.next()) {
int id = q.value(0).toInt();
QString name = q.value(1).toString();
loadGroups();
loadConnections();
}
auto g = std::make_shared<ConnectionGroup>();
g->conngroup_id = id;
g->name = name;
m_groups.push_back(g);
}
void ConnectionTreeModel::loadGroups()
{
auto stmt = m_db.Prepare("SELECT conngroup_id, gname FROM conngroup;");
while (stmt.Step())
{
auto g = std::make_shared<ConnectionGroup>();
g->conngroup_id = stmt.ColumnInteger(0);
g->name = stmt.ColumnText(1);
m_groups.push_back(g);
}
}
q.prepare("SELECT uuid, cname, conngroup_id, password "
"FROM connection ORDER BY conngroup_id, cname;");
if (!q.exec()) {
// auto err = q_create_table.lastError();
// return { false, err };
throw std::runtime_error("Loading groups failed");
}
while (q.next()) {
auto cc = std::make_shared<ConnectionConfig>();
cc->setUuid(q.value(0).toUuid());
cc->setName(q.value(1).toString());
cc->setHost(q.value(3).toString());
cc->setHostAddr(q.value(4).toString());
cc->setPort(static_cast<uint16_t>(q.value(5).toInt()));
cc->setUser(q.value(6).toString());
cc->setDbname(q.value(7).toString());
cc->setSslMode(static_cast<SslMode>(q.value(8).toInt()));
cc->setSslCert(q.value(9).toString());
cc->setSslKey(q.value(10).toString());
cc->setSslRootCert(q.value(11).toString());
cc->setSslCrl(q.value(12).toString());
cc->setEncodedPassword(q.value(13).toByteArray());
void ConnectionTreeModel::loadConnections()
{
auto stmt = m_db.Prepare(
"SELECT uuid, cname, conngroup_id, password "
"FROM connection ORDER BY conngroup_id, cname;");
int group_id = q.value(2).toInt();
auto find_res = std::find_if(m_groups.begin(), m_groups.end(),
[group_id] (auto item) { return item->conngroup_id == group_id; });
if (find_res != m_groups.end()) {
(*find_res)->add(cc);
}
else {
throw std::runtime_error("conngroup missing");
}
}
while (stmt.Step()) {
auto cc = std::make_shared<ConnectionConfig>();
cc->setUuid(QUuid::fromString(stmt.ColumnText(0)));
cc->setName(stmt.ColumnText(1));
cc->setEncodedPassword(QByteArray::fromBase64(stmt.ColumnCharPtr(3), QByteArray::Base64Encoding));
loadConnectionParameters(*cc);
int group_id = stmt.ColumnInteger(2);
auto find_res = std::find_if(m_groups.begin(), m_groups.end(),
[group_id] (auto item) { return item->conngroup_id == group_id; });
if (find_res != m_groups.end()) {
(*find_res)->add(cc);
}
else {
throw std::runtime_error("conngroup missing");
}
}
}
void ConnectionTreeModel::loadConnectionParameters(ConnectionConfig &cc)
{
auto stmt = m_db.Prepare(
"SELECT pname, pvalue \n"
"FROM connection_parameter\n"
"WHERE connection_uuid=?1");
stmt.Bind(1, cc.uuid().toString());
while (stmt.Step())
{
cc.setParameter(
stmt.ColumnText(0),
stmt.ColumnText(1)
);
}
}
QVariant ConnectionTreeModel::data(const QModelIndex &index, int role) const
@ -408,15 +377,11 @@ bool ConnectionTreeModel::removeRows(int row, int count, const QModelIndex &pare
auto grp = m_groups[parent.row()];
for (int i = 0; i < count; ++i) {
QUuid uuid = grp->connections().at(row + i)->uuid();
QSqlQuery q(m_db);
q.prepare(
auto stmt = m_db.Prepare(
"DELETE FROM connection "
" WHERE uuid=:uuid");
q.bindValue(":uuid", uuid);
if (!q.exec()) {
auto err = q.lastError();
throw std::runtime_error("QqlError");
}
" WHERE uuid=?0");
stmt.Bind(0, uuid.toString());
stmt.Step();
}
beginRemoveRows(parent, row, row + count - 1);
SCOPE_EXIT { endRemoveRows(); };
@ -454,14 +419,8 @@ void ConnectionTreeModel::save(const QString &group_name, const ConnectionConfig
// We assume the model is in sync with the DB as the DB should not be shared!
int new_grp_idx = findGroup(group_name);
if (new_grp_idx < 0) {
// Group not found we are g
auto add_grp_res = addGroup(group_name);
if (std::holds_alternative<int>(add_grp_res)) {
new_grp_idx = std::get<int>(add_grp_res);
}
else {
throw std::runtime_error("SqlError1");
}
// Group not found we are g
new_grp_idx = addGroup(group_name);
}
auto new_grp = m_groups[new_grp_idx];
@ -470,15 +429,8 @@ void ConnectionTreeModel::save(const QString &group_name, const ConnectionConfig
beginInsertRows(parent, idx, idx);
SCOPE_EXIT { endInsertRows(); };
auto node = std::make_shared<ConnectionConfig>(cc);
new_grp->add(node);
auto save_res = saveToDb(*node);
if (save_res) {
QString msg = save_res->text()
% "\n" % save_res->driverText()
% "\n" % save_res->databaseText();
throw std::runtime_error(msg.toUtf8().data());
}
new_grp->add(node);
saveToDb(*node);
}
void ConnectionTreeModel::save(const ConnectionConfig &cc)
@ -523,17 +475,13 @@ int ConnectionTreeModel::findGroup(const QString &name) const
return -1;
}
std::variant<int, QSqlError> ConnectionTreeModel::addGroup(const QString &group_name)
int ConnectionTreeModel::addGroup(const QString &group_name)
{
QSqlQuery q(m_db);
q.prepare("INSERT INTO conngroup (gname) VALUES (:name)");
q.bindValue(":name", group_name);
if (!q.exec()) {
auto err = q.lastError();
return { err };
}
auto cg = std::make_shared<ConnectionGroup>();
cg->conngroup_id = q.lastInsertId().toInt();
auto stmt = m_db.Prepare("INSERT INTO conngroup (gname) VALUES (?1)");
stmt.Bind(1, group_name);
auto cg = std::make_shared<ConnectionGroup>();
cg->conngroup_id = m_db.LastInsertRowId();
cg->name = group_name;
int row = m_groups.size();
@ -543,27 +491,21 @@ std::variant<int, QSqlError> ConnectionTreeModel::addGroup(const QString &group_
return row;
}
std::optional<QSqlError> ConnectionTreeModel::removeGroup(int row)
void ConnectionTreeModel::removeGroup(int row)
{
beginRemoveRows({}, row, row);
SCOPE_EXIT { endRemoveRows(); };
auto id = m_groups[row]->conngroup_id;
QSqlQuery q(m_db);
q.prepare("DELETE FROM connection WHERE conngroup_id=:id");
q.bindValue(":id", id);
if (!q.exec()) {
auto err = q.lastError();
return { err };
}
q.prepare("DELETE FROM conngroup WHERE conngroup_id=:id");
q.bindValue(":id", id);
if (!q.exec()) {
auto err = q.lastError();
return { err };
}
auto stmt = m_db.Prepare("DELETE FROM connection WHERE conngroup_id=?1");
stmt.Bind(1, id);
stmt.Step();
stmt = m_db.Prepare("DELETE FROM conngroup WHERE conngroup_id=?1");
stmt.Bind(1, id);
stmt.Step();
m_groups.remove(row);
return {};
}
int ConnectionTreeModel::findGroup(int conngroup_id) const
@ -591,9 +533,9 @@ ConnectionGroup *ConnectionTreeModel::getGroupFromModelIndex(QModelIndex index)
return dynamic_cast<ConnectionGroup*>(node);
}
std::optional<QSqlError> ConnectionTreeModel::saveToDb(const ConnectionConfig &cc)
void ConnectionTreeModel::saveToDb(const ConnectionConfig &cc)
{
return SaveConnectionConfig(m_db, cc, cc.parent()->conngroup_id);
SaveConnectionConfig(m_db, cc, cc.parent()->conngroup_id);
}

View file

@ -11,8 +11,7 @@
#include <variant>
#include <QVector>
#include <QSqlError>
class QSqlDatabase;
#include "sqlite/SQLiteConnection.h"
class ConnectionTreeModel : public QAbstractItemModel {
Q_OBJECT
@ -27,7 +26,7 @@ public:
ColCount
};
ConnectionTreeModel(QObject *parent, QSqlDatabase &db);
ConnectionTreeModel(QObject *parent, SQLiteConnection &db);
void load();
@ -61,8 +60,8 @@ public:
void save(const ConnectionConfig &cc);
void clearAllPasswords();
/// Create a new group in the DB and place in the tree
std::variant<int, QSqlError> addGroup(const QString &group_name);
std::optional<QSqlError> removeGroup(int row);
int addGroup(const QString &group_name);
void removeGroup(int row);
int findGroup(int conngroup_id) const;
static ConnectionConfig* getConfigFromModelIndex(QModelIndex index);
@ -71,7 +70,7 @@ public:
private:
using Groups = QVector<std::shared_ptr<ConnectionGroup>>;
QSqlDatabase &m_db;
SQLiteConnection &m_db;
Groups m_groups;
/// Finds the connection with the specified uuid and returns
@ -79,8 +78,11 @@ private:
std::tuple<int, int> findConfig(const QUuid uuid) const;
int findGroup(const QString &name) const;
std::optional<QSqlError> saveToDb(const ConnectionConfig &cc);
void saveToDb(const ConnectionConfig &cc);
void loadGroups();
void loadConnections();
void loadConnectionParameters(ConnectionConfig &cc);
// QAbstractItemModel interface
public:
virtual Qt::DropActions supportedDropActions() const override;

View file

@ -30,15 +30,7 @@ MasterController::~MasterController()
void MasterController::init()
{
m_userConfigDatabase = QSqlDatabase::addDatabase("QSQLITE");
m_userConfigDatabase.setDatabaseName(GetUserConfigDatabaseName());
if (!m_userConfigDatabase.open()) {
qDebug() << "Error: connection with database fail";
}
else {
qDebug() << "Database: connection ok";
}
m_userConfigDatabase.Open(GetUserConfigDatabaseName());
m_connectionController = new ConnectionController(this);
m_connectionController->init();
@ -62,7 +54,7 @@ ConnectionController *MasterController::connectionController()
return m_connectionController;
}
QSqlDatabase& MasterController::userConfigDatabase()
SQLiteConnection& MasterController::userConfigDatabase()
{
return m_userConfigDatabase;
}

View file

@ -2,11 +2,11 @@
#define MASTERCONTROLLER_H
#include <QObject>
#include <QSqlDatabase>
#include <atomic>
#include <future>
#include <map>
#include <memory>
#include "sqlite/SQLiteConnection.h"
class ConnectionController;
@ -23,14 +23,14 @@ public:
void init();
ConnectionController* connectionController();
QSqlDatabase& userConfigDatabase();
SQLiteConnection& userConfigDatabase();
signals:
public slots:
private:
QSqlDatabase m_userConfigDatabase;
SQLiteConnection m_userConfigDatabase;
ConnectionController* m_connectionController = nullptr;
};