diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp index 13ed3c1b..78b5990a 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp @@ -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" diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h index 2986bd41..d3c60737 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h @@ -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" diff --git a/libnymea-core/jsonrpc/usershandler.cpp b/libnymea-core/jsonrpc/usershandler.cpp index 1f72026e..4ec96e6a 100644 --- a/libnymea-core/jsonrpc/usershandler.cpp +++ b/libnymea-core/jsonrpc/usershandler.cpp @@ -159,8 +159,11 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap ¶ms) QString displayName = params.value("displayName").toString(); QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList(); Types::PermissionScopes scopes = Types::scopesFromStringList(scopesList); + QList 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(status)); @@ -310,7 +313,8 @@ JsonReply *UsersHandler::SetUserScopes(const QVariantMap ¶ms, 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 allowedThingIds = Types::thingIdsFromStringList(params.value("allowedThingIds").toStringList()); + UserManager::UserError error = m_userManager->setUserScopes(username, scopes, allowedThingIds); QVariantMap returns; returns.insert("error", enumValueName(error)); return createReply(returns); diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index eb00b7ec..b0a59f0b 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -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" diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index 1715cbf5..e95701d7 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -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 &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 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 &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 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))) { diff --git a/libnymea-core/usermanager/usermanager.h b/libnymea-core/usermanager/usermanager.h index 391cf1b4..3c233a13 100644 --- a/libnymea-core/usermanager/usermanager.h +++ b/libnymea-core/usermanager/usermanager.h @@ -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 &allowedThingIds = QList()); 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 &allowedThingIds = QList()); UserError setUserInfo(const QString &username, const QString &email, const QString &displayName); bool pushButtonAuthAvailable() const; diff --git a/libnymea/types/typeutils.cpp b/libnymea/types/typeutils.cpp index 837626ac..76128f3d 100644 --- a/libnymea/types/typeutils.cpp +++ b/libnymea/types/typeutils.cpp @@ -44,6 +44,24 @@ QString Types::scopeToString(Types::PermissionScope scope) return metaEnum.valueToKey(scope); } +QStringList Types::thingIdsToStringList(const QList &thingIds) +{ + QStringList stringList; + foreach (const ThingId &thingId, thingIds) + stringList.append(thingId.toString()); + + return stringList; +} + +QList Types::thingIdsFromStringList(const QStringList &stringList) +{ + QList thingIds; + foreach (const QString &idString, stringList) + thingIds.append(ThingId(idString)); + + return thingIds; +} + Types::PermissionScope Types::scopeFromString(const QString &scopeString) { QMetaEnum metaEnum = QMetaEnum::fromType(); diff --git a/libnymea/typeutils.h b/libnymea/typeutils.h index 10a25377..6f9a1152 100644 --- a/libnymea/typeutils.h +++ b/libnymea/typeutils.h @@ -216,6 +216,10 @@ public: static QStringList scopesToStringList(PermissionScopes scopes); static QString scopeToString(PermissionScope scope); + static QStringList thingIdsToStringList(const QList &thingIds); + static QList thingIdsFromStringList(const QStringList &stringList); + + enum LoggingType { LoggingTypeDiscrete, LoggingTypeSampled,