UserManager: Update user database and migrate to version 2

This commit is contained in:
Simon Stürz 2025-10-23 13:37:10 +02:00
parent e638c8cab2
commit 71cd3561b6
8 changed files with 158 additions and 38 deletions

View File

@ -43,6 +43,7 @@
#include "jsonrpc/jsonhandler.h"
#include "jsonvalidator.h"
#include "nymeacore.h"
#include "usermanager/usermanager.h"
#include "integrations/thingmanager.h"
#include "integrations/integrationplugin.h"
#include "integrations/thing.h"

View File

@ -28,7 +28,6 @@
#include "jsonrpc/jsonrpcserver.h"
#include "jsonrpc/jsonhandler.h"
#include "transportinterface.h"
#include "usermanager/usermanager.h"
#include "types/thingclass.h"
#include "types/action.h"

View File

@ -159,8 +159,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap &params)
QString displayName = params.value("displayName").toString();
QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList();
Types::PermissionScopes scopes = Types::scopesFromStringList(scopesList);
QList<ThingId> allowedThingIds;
foreach (const QString &thingIdString, params.value("allowedThingIds").toStringList())
allowedThingIds.append(ThingId(thingIdString));
UserManager::UserError status = m_userManager->createUser(username, password, email, displayName, scopes);
UserManager::UserError status = m_userManager->createUser(username, password, email, displayName, scopes, allowedThingIds);
QVariantMap returns;
returns.insert("error", enumValueName<UserManager::UserError>(status));
@ -310,7 +313,8 @@ JsonReply *UsersHandler::SetUserScopes(const QVariantMap &params, const JsonCont
Q_UNUSED(context)
QString username = params.value("username").toString();
Types::PermissionScopes scopes = Types::scopesFromStringList(params.value("scopes").toStringList());
UserManager::UserError error = m_userManager->setUserScopes(username, scopes);
QList<ThingId> allowedThingIds = Types::thingIdsFromStringList(params.value("allowedThingIds").toStringList());
UserManager::UserError error = m_userManager->setUserScopes(username, scopes, allowedThingIds);
QVariantMap returns;
returns.insert("error", enumValueName<UserManager::UserError>(error));
return createReply(returns);

View File

@ -36,6 +36,7 @@
#include "scriptengine/scriptengine.h"
#include "jsonrpc/scriptshandler.h"
#include "jsonrpc/debughandler.h"
#include "usermanager/usermanager.h"
#include "version.h"
#include "integrations/thingmanagerimplementation.h"

View File

@ -147,13 +147,14 @@ UserInfoList UserManager::users() const
info.setEmail(resultQuery.value("email").toString());
info.setDisplayName(resultQuery.value("displayName").toString());
info.setScopes(Types::scopesFromStringList(resultQuery.value("scopes").toString().split(',')));
info.setAllowedThingIds(Types::thingIdsFromStringList(resultQuery.value("allowedThingIds").toString().split(',')));
users.append(info);
}
return users;
}
/*! Creates a new user with the given \a username and \a password. Returns the \l UserError to inform about the result. */
UserManager::UserError UserManager::createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes)
UserManager::UserError UserManager::createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds)
{
if (!validateUsername(username)) {
qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username;
@ -170,6 +171,17 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
return UserErrorInconsistantScopes;
}
// Verify thing IDs, if there is no thing with this id, we don't save it and it will not be verified.
// We don't return an error, the thing might have dissapeared
QList<ThingId> thingIds;
foreach (const ThingId &thingId, allowedThingIds) {
if (NymeaCore::instance()->thingManager()->configuredThings().findById(thingId) == nullptr) {
qCWarning(dcUserManager()) << "Cannot set user scope for" << username << "because there is no thing with ID ";
} else {
thingIds.append(thingId);
}
}
QSqlQuery checkForDuplicateUserQuery(m_db);
checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = ?;");
// Note: We're using toLower() on the username mainly for the reason that in old versions the username used to be an email address
@ -183,13 +195,14 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
QSqlQuery query(m_db);
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) VALUES(?, ?, ?, ?, ?, ?);");
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes, allowedThingIds) VALUES(?, ?, ?, ?, ?, ?, ?);");
query.addBindValue(username.toLower());
query.addBindValue(email);
query.addBindValue(displayName);
query.addBindValue(QString::fromUtf8(hashedPassword));
query.addBindValue(QString::fromUtf8(salt));
query.addBindValue(Types::scopesToStringList(scopes).join(','));
query.addBindValue(Types::thingIdsToStringList(thingIds).join(','));
query.exec();
if (query.lastError().type() != QSqlError::NoError) {
qCWarning(dcUserManager) << "Error creating user:" << query.lastError().databaseText() << query.lastError().driverText();
@ -271,7 +284,7 @@ UserManager::UserError UserManager::removeUser(const QString &username)
return UserErrorNoError;
}
UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes)
UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds)
{
if (!validateScopes(scopes)) {
// The method warns about he specific validation
@ -279,13 +292,25 @@ UserManager::UserError UserManager::setUserScopes(const QString &username, Types
}
// Verify thing IDs, if there is no thing with this id, we don't save it and it will not be verified.
// We don't return an error, the thing might have dissapeared
QList<ThingId> thingIds;
foreach (const ThingId &thingId, allowedThingIds) {
if (NymeaCore::instance()->thingManager()->configuredThings().findById(thingId) == nullptr) {
qCWarning(dcUserManager()) << "Cannot set user scope for" << username << "because there is no thing with ID ";
} else {
thingIds.append(thingId);
}
}
QString scopesString = Types::scopesToStringList(scopes).join(',');
QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(',');
QSqlQuery setScopesQuery(m_db);
setScopesQuery.prepare("UPDATE users SET scopes = ? WHERE username = ?");
setScopesQuery.addBindValue(scopesString);
setScopesQuery.addBindValue(username);
setScopesQuery.exec();
if (setScopesQuery.lastError().type() != QSqlError::NoError) {
setScopesQuery.prepare("UPDATE users SET scopes = :scopes, allowedThingIds = :allowedThingIds WHERE username = :username");
setScopesQuery.bindValue(":scopes", scopesString);
setScopesQuery.bindValue(":allowedThingIds", allowedThingIdsString);
setScopesQuery.bindValue(":username", username);
if (!setScopesQuery.exec()) {
qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText();
return UserErrorBackendError;
}
@ -424,6 +449,7 @@ UserInfo UserManager::userInfo(const QString &username) const
userInfo.setEmail(getUserQuery.value("email").toString());
userInfo.setDisplayName(getUserQuery.value("displayName").toString());
userInfo.setScopes(Types::scopesFromStringList(getUserQuery.value("scopes").toString().split(',')));
userInfo.setAllowedThingIds(Types::thingIdsFromStringList(getUserQuery.value("allowedThingIds").toString().split(',')));
return userInfo;
}
@ -476,7 +502,6 @@ TokenInfo UserManager::tokenInfo(const QByteArray &token) const
TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const
{
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = \"%1\";")
.arg(tokenId.toString());
@ -572,26 +597,32 @@ bool UserManager::initDB()
}
int currentVersion = -1;
int newVersion = 1;
int newVersion = 2;
if (m_db.tables().contains("metadata")) {
QSqlQuery query(m_db);
if (!query.exec("SELECT data FROM metadata WHERE `key` = 'version';")) {
if (!query.exec("SELECT data FROM metadata WHERE key = 'version';")) {
qCWarning(dcUserManager()) << "Unable to execute SQL query" << query.executedQuery() << m_db.lastError().databaseText() << m_db.lastError().driverText();
} else if (query.next()) {
currentVersion = query.value("data").toInt();
qCInfo(dcUserManager()) << "Current database version is" << currentVersion;
if (currentVersion == newVersion) {
qCInfo(dcUserManager()) << "The database version is up to date";
}
}
}
if (!m_db.tables().contains("users")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata...";
qCDebug(dcUserManager()) << "No \"users\" table found. Creating the table...";
QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE PRIMARY KEY, email VARCHAR(40), displayName VARCHAR(40), password VARCHAR(100), salt VARCHAR(100), scopes TEXT);") || m_db.lastError().isValid()) {
if (!query.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE PRIMARY KEY, email VARCHAR(40), displayName VARCHAR(40), password VARCHAR(100), salt VARCHAR(100), scopes TEXT, allowedThingIds TEXT);") || m_db.lastError().isValid()) {
dumpDBError("Error initializing user database (table users).");
m_db.close();
return false;
}
} else {
if (currentVersion < 1) {
qCDebug(dcUserManager()) << "Start user table database migration to version 1";
QSqlQuery query = QSqlQuery(m_db);
if (!query.exec("ALTER TABLE users ADD COLUMN scopes TEXT;") || m_db.lastError().isValid()) {
dumpDBError("Error migrating user database (table users).");
@ -631,44 +662,106 @@ bool UserManager::initDB()
m_db.close();
return false;
}
currentVersion = 1;
qCDebug(dcUserManager()) << "Migrated successfully users table to database version 1";
}
if (currentVersion < 2) {
// - Add new "allowedThingIds" row into the users table
// - New permission has been added "PermissionScopeAccessAllThings", the existing users require
// all this permission in order to have an unchainged behavior
qCDebug(dcUserManager()) << "Migrating user table to version 2";
// - Add new "allowedThingIds" row into the users table, it remains is empty at this point
QSqlQuery query = QSqlQuery(m_db);
if (!query.exec("ALTER TABLE users ADD COLUMN allowedThingIds TEXT;") || m_db.lastError().isValid()) {
dumpDBError("Error migrating user database (table users).");
m_db.close();
return false;
}
if (!m_db.transaction()) {
dumpDBError("Error starting transaction for migrating user database (table users).");
return false;
}
QSqlQuery selectQuery(m_db);
if (!selectQuery.exec("SELECT username, scopes FROM users")) {
dumpDBError("Select failed: " + selectQuery.lastError().text());
return false;
}
QSqlQuery updateQuery(m_db);
updateQuery.prepare("UPDATE users SET scopes = :scopes WHERE username = :username");
while (selectQuery.next()) {
QString username = selectQuery.value("username").toString();
Types::PermissionScopes scopes = Types::scopesFromStringList(selectQuery.value("scopes").toString().split(','));
// In case this is an admin, make sure we store only the Admin scope
if (!scopes.testFlag(Types::PermissionScopeAdmin)) {
scopes.setFlag(Types::PermissionScopeAccessAllThings);
}
updateQuery.bindValue(":scopes", Types::scopesToStringList(scopes).join(','));
updateQuery.bindValue(":username", username);
if (!updateQuery.exec()) {
qCWarning(dcUserManager()) << "Update failed for username" << username << ":" << updateQuery.lastError().text();
m_db.rollback();
return false;
}
}
if (!m_db.commit()) {
dumpDBError("Error migrating user database (table users) to version 2. Rollback.");
m_db.rollback();
return false;
}
qCDebug(dcUserManager()) << "Migrated successfully users table to database version 2";
}
}
if (!m_db.tables().contains("tokens")) {
qCDebug(dcUserManager()) << "Empty user database. Setting up metadata...";
qCDebug(dcUserManager()) << "No \"tokens\" table found. Creating the table...";
QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));") || m_db.lastError().isValid()) {
dumpDBError("Error initializing user database (table tokens).");
dumpDBError("Error initializing user database (table tokens)");
m_db.close();
return false;
}
}
if (m_db.tables().contains("metadata")) {
if (currentVersion < newVersion) {
QSqlQuery query(m_db);
if (!query.exec(QString("UPDATE metadata SET data = %1 WHERE `key` = 'version')").arg(newVersion)) || m_db.lastError().isValid()) {
dumpDBError("Error updating up user database schema version!");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Successfully migrated user database.";
}
} else {
if (!m_db.tables().contains("metadata")) {
qCDebug(dcUserManager()) << "No \"metadata\" table found. Creating the table...";
QSqlQuery query(m_db);
if (!query.exec("CREATE TABLE metadata (`key` VARCHAR(10), data VARCHAR(40));") || m_db.lastError().isValid()) {
if (!query.exec("CREATE TABLE metadata (key VARCHAR(10), data VARCHAR(40));") || m_db.lastError().isValid()) {
dumpDBError("Error setting up user database (table metadata)!");
m_db.close();
return false;
}
query = QSqlQuery(m_db);
if (!query.exec(QString("INSERT INTO metadata (`key`, `data`) VALUES ('version', %1);").arg(newVersion)) || m_db.lastError().isValid()) {
query.prepare("INSERT INTO metadata (key, data) VALUES ('version', :version);");
query.bindValue(":version", newVersion);
if (!query.exec() || m_db.lastError().isValid()) {
dumpDBError("Error setting up user database (setting version metadata)!");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Successfully initialized user database.";
} else {
// All migrations have been done
if (currentVersion < newVersion) {
QSqlQuery query(m_db);
query.prepare("UPDATE metadata SET data = :version WHERE key = 'version'");
query.bindValue(":version", newVersion);
if (!query.exec() || m_db.lastError().isValid()) {
dumpDBError("Error updating database version");
m_db.close();
return false;
}
qCInfo(dcUserManager()) << "Finished database migration to version" << newVersion;
}
}
@ -700,17 +793,17 @@ bool UserManager::initDB()
}
}
qCDebug(dcUserManager()) << "User database initialized successfully.";
qCDebug(dcUserManager()) << "User database initialized successfully";
return true;
}
void UserManager::rotate(const QString &dbName)
{
int index = 1;
while (QFileInfo(QString("%1.%2").arg(dbName).arg(index)).exists()) {
while (QFileInfo::exists(QString("%1.%2").arg(dbName).arg(index))) {
index++;
}
qCDebug(dcUserManager()) << "Backing up old database file to" << QString("%1.%2").arg(dbName).arg(index);
QFile f(dbName);
if (!f.rename(QString("%1.%2").arg(dbName).arg(index))) {

View File

@ -56,10 +56,10 @@ public:
bool initRequired() const;
UserInfoList users() const;
UserError createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes);
UserError createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds = QList<ThingId>());
UserError changePassword(const QString &username, const QString &newPassword);
UserError removeUser(const QString &username);
UserError setUserScopes(const QString &username, Types::PermissionScopes scopes);
UserError setUserScopes(const QString &username, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds = QList<ThingId>());
UserError setUserInfo(const QString &username, const QString &email, const QString &displayName);
bool pushButtonAuthAvailable() const;

View File

@ -44,6 +44,24 @@ QString Types::scopeToString(Types::PermissionScope scope)
return metaEnum.valueToKey(scope);
}
QStringList Types::thingIdsToStringList(const QList<ThingId> &thingIds)
{
QStringList stringList;
foreach (const ThingId &thingId, thingIds)
stringList.append(thingId.toString());
return stringList;
}
QList<ThingId> Types::thingIdsFromStringList(const QStringList &stringList)
{
QList<ThingId> thingIds;
foreach (const QString &idString, stringList)
thingIds.append(ThingId(idString));
return thingIds;
}
Types::PermissionScope Types::scopeFromString(const QString &scopeString)
{
QMetaEnum metaEnum = QMetaEnum::fromType<PermissionScope>();

View File

@ -216,6 +216,10 @@ public:
static QStringList scopesToStringList(PermissionScopes scopes);
static QString scopeToString(PermissionScope scope);
static QStringList thingIdsToStringList(const QList<ThingId> &thingIds);
static QList<ThingId> thingIdsFromStringList(const QStringList &stringList);
enum LoggingType {
LoggingTypeDiscrete,
LoggingTypeSampled,