938 lines
41 KiB
C++
938 lines
41 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea.
|
|
*
|
|
* nymea is free software: you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public License
|
|
* as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* nymea is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
/*!
|
|
\class nymeaserver::UserManager
|
|
\brief This class represents the manager for the users in nymead.
|
|
|
|
\ingroup user
|
|
\inmodule core
|
|
|
|
The user manager is responsible for managing the user database, tokens and authentication. The user manager
|
|
creates a user database where all relevant information will be stored.
|
|
|
|
\sa TokenInfo, PushButtonDBusService
|
|
*/
|
|
|
|
/*! \enum nymeaserver::UserManager::UserError
|
|
|
|
This enum represents the possible errors the \l{UserManager} can have.
|
|
|
|
\value UserErrorNoError
|
|
No error occurred. Everything is ok.4
|
|
\value UserErrorBackendError
|
|
Something went wrong in the manager. This is probably caused by a database error.
|
|
\value UserErrorInvalidUserId
|
|
The given user name is not valid.
|
|
\value UserErrorDuplicateUserId
|
|
The given user name already exits. Please use a different user name.
|
|
\value UserErrorBadPassword
|
|
The given password is to weak. Please use a stronger password.
|
|
\value UserErrorTokenNotFound
|
|
The given token is unknown to the UserManager.
|
|
\value UserErrorPermissionDenied
|
|
The permission is denied. Either invalid username, password or token.
|
|
*/
|
|
|
|
/*! \fn void nymeaserver::UserManager::pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token);
|
|
This signal is emitted when the push authentication for the given \a transactionId is finished.
|
|
If \a success is true, the resulting \a token contains a non empty string.
|
|
|
|
\sa requestPushButtonAuth
|
|
*/
|
|
|
|
#include "usermanager.h"
|
|
#include "nymeasettings.h"
|
|
#include "loggingcategories.h"
|
|
#include "pushbuttondbusservice.h"
|
|
#include "nymeacore.h"
|
|
|
|
#include <QUuid>
|
|
#include <QCryptographicHash>
|
|
#include <QSqlQuery>
|
|
#include <QVariant>
|
|
#include <QSqlError>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QFileInfo>
|
|
#include <QRegularExpression>
|
|
|
|
namespace nymeaserver {
|
|
|
|
/*! Constructs a new UserManager with the given \a dbName and \a parent. */
|
|
UserManager::UserManager(const QString &dbName, QObject *parent):
|
|
QObject(parent)
|
|
{
|
|
m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), "users");
|
|
m_db.setDatabaseName(dbName);
|
|
|
|
qCDebug(dcUserManager()) << "Opening user database" << m_db.databaseName();
|
|
|
|
if (!m_db.isValid()) {
|
|
qCWarning(dcUserManager()) << "The database is not valid:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
|
|
rotate(m_db.databaseName());
|
|
}
|
|
|
|
if (!initDB()) {
|
|
qCWarning(dcUserManager()) << "Error initializing user database. Trying to correct it.";
|
|
if (QFileInfo::exists(m_db.databaseName())) {
|
|
rotate(m_db.databaseName());
|
|
if (!initDB()) {
|
|
qCWarning(dcUserManager()) << "Error fixing user database. Giving up. Users can't be stored.";
|
|
}
|
|
}
|
|
}
|
|
|
|
m_pushButtonDBusService = new PushButtonDBusService("/io/nymea/nymead/UserManager", this);
|
|
connect(m_pushButtonDBusService, &PushButtonDBusService::pushButtonPressed, this, &UserManager::onPushButtonPressed);
|
|
m_pushButtonTransaction = QPair<int, QString>(-1, QString());
|
|
}
|
|
|
|
/*! Will return true if the database is working fine but doesn't have any information on users whatsoever.
|
|
* That is, neither a user nor an anonymous token.
|
|
* This may be used to determine whether a first-time setup is required.
|
|
*/
|
|
bool UserManager::initRequired() const
|
|
{
|
|
QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens;");
|
|
QSqlQuery resultQuery(m_db);
|
|
if (!resultQuery.exec(getTokensQuery)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokensQuery << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return false;
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery;
|
|
// Note: do not return true in case the database access fails.
|
|
return false;
|
|
}
|
|
|
|
return users().isEmpty() && !resultQuery.first();
|
|
}
|
|
|
|
/*! Returns the list of user names for this UserManager. */
|
|
UserInfoList UserManager::users() const
|
|
{
|
|
UserInfoList users;
|
|
QString userQuery("SELECT * FROM users;");
|
|
QSqlQuery resultQuery(m_db);
|
|
if (!resultQuery.exec(userQuery)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << userQuery << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return users;
|
|
}
|
|
|
|
while (resultQuery.next()) {
|
|
UserInfo info = UserInfo(resultQuery.value("username").toString());
|
|
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, const QList<ThingId> &allowedThingIds)
|
|
{
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username;
|
|
return UserErrorInvalidUserId;
|
|
}
|
|
|
|
if (!validatePassword(password)) {
|
|
qCWarning(dcUserManager) << "Password failed character validation. Must contain a letter, a number and a special charactar. Minimum length: 8";
|
|
return UserErrorBadPassword;
|
|
}
|
|
|
|
if (!validateScopes(scopes)) {
|
|
// The method warns about he specific validation
|
|
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
|
|
checkForDuplicateUserQuery.addBindValue(username.toLower());
|
|
checkForDuplicateUserQuery.exec();
|
|
if (checkForDuplicateUserQuery.first()) {
|
|
qCWarning(dcUserManager) << "Username already in use";
|
|
return UserErrorDuplicateUserId;
|
|
}
|
|
|
|
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, 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();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
qCInfo(dcUserManager()) << "New user" << username << "added to the system with permissions:" << Types::scopesToStringList(scopes);
|
|
emit userAdded(username);
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
UserManager::UserError UserManager::changePassword(const QString &username, const QString &newPassword)
|
|
{
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Invalid username:" << username;
|
|
return UserErrorInvalidUserId;
|
|
}
|
|
|
|
if (!validatePassword(newPassword)) {
|
|
qCWarning(dcUserManager) << "Password failed character validation. Must contain a letter, a number and a special charactar. Minimum length: 8";
|
|
return UserErrorBadPassword;
|
|
}
|
|
|
|
QString checkForUserExistingQueryString = QString("SELECT * FROM users WHERE lower(username) = \"%1\";").arg(username.toLower());
|
|
QSqlQuery checkForUserExistingQuery(m_db);
|
|
if (!checkForUserExistingQuery.exec(checkForUserExistingQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << checkForUserExistingQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
if (!checkForUserExistingQuery.first()) {
|
|
qCWarning(dcUserManager) << "Username does not exist.";
|
|
return UserErrorInvalidUserId;
|
|
}
|
|
|
|
// Update the password
|
|
QByteArray salt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8();
|
|
QByteArray hashedPassword = QCryptographicHash::hash(QString(newPassword + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
|
|
QString updatePasswordQueryString = QString("UPDATE users SET password = \"%1\", salt = \"%2\" WHERE lower(username) = \"%3\";")
|
|
.arg(QString::fromUtf8(hashedPassword))
|
|
.arg(QString::fromUtf8(salt))
|
|
.arg(username.toLower());
|
|
|
|
QSqlQuery updatePasswordQuery(m_db);
|
|
if (!updatePasswordQuery.exec(updatePasswordQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << updatePasswordQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error updating password for user:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
qCDebug(dcUserManager()) << "Password updated for user" << username;
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
UserManager::UserError UserManager::removeUser(const QString &username)
|
|
{
|
|
QString dropUserQueryString = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower());
|
|
QSqlQuery dropUserQuery(m_db);
|
|
if (!dropUserQuery.exec(dropUserQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << dropUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
if (dropUserQuery.numRowsAffected() == 0)
|
|
return UserErrorInvalidUserId;
|
|
|
|
QString dropTokensQueryString = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower());
|
|
QSqlQuery dropTokensQuery(m_db);
|
|
if (!dropTokensQuery.exec(dropTokensQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << dropTokensQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
emit userRemoved(username);
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes, const QList<ThingId> &allowedThingIds)
|
|
{
|
|
if (!validateScopes(scopes)) {
|
|
// The method warns about he specific validation
|
|
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);
|
|
}
|
|
}
|
|
|
|
QString scopesString = Types::scopesToStringList(scopes).join(',');
|
|
QString allowedThingIdsString = Types::thingIdsToStringList(thingIds).join(',');
|
|
QSqlQuery setScopesQuery(m_db);
|
|
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;
|
|
}
|
|
|
|
emit userChanged(username);
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName)
|
|
{
|
|
QSqlQuery query(m_db);
|
|
query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;");
|
|
query.addBindValue(email);
|
|
query.addBindValue(displayName);
|
|
query.addBindValue(username);
|
|
query.exec();
|
|
if (query.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
|
|
return UserErrorBackendError;
|
|
}
|
|
emit userChanged(username);
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
/*! Returns true if the push button authentication is available for this system. */
|
|
bool UserManager::pushButtonAuthAvailable() const
|
|
{
|
|
return m_pushButtonDBusService->agentAvailable();
|
|
}
|
|
|
|
/*! Authenticated the given \a username with the given \a password for the \a deviceName. If the authentication was
|
|
successful, the token will be returned, otherwise the return value will be an empty byte array.
|
|
*/
|
|
QByteArray UserManager::authenticate(const QString &username, const QString &password, const QString &deviceName)
|
|
{
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Authenticate: Username did not pass validation:" << username;
|
|
return QByteArray();
|
|
}
|
|
|
|
QSqlQuery passwordQuery(m_db);
|
|
passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = ?;");
|
|
passwordQuery.addBindValue(username.toLower());
|
|
passwordQuery.exec();
|
|
if (!passwordQuery.first()) {
|
|
qCWarning(dcUserManager) << "No such username" << username;
|
|
return QByteArray();
|
|
}
|
|
QByteArray salt = passwordQuery.value("salt").toByteArray();
|
|
QByteArray hashedPassword = passwordQuery.value("password").toByteArray();
|
|
|
|
if (hashedPassword != QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64()) {
|
|
qCWarning(dcUserManager) << "Authentication error for user:" << username;
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
|
|
QString storeTokenQueryString = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
|
|
.arg(QUuid::createUuid().toString())
|
|
.arg(username.toLower())
|
|
.arg(QString::fromUtf8(token))
|
|
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
|
|
.arg(deviceName);
|
|
|
|
QSqlQuery storeTokenQuery(m_db);
|
|
if (!storeTokenQuery.exec(storeTokenQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << storeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return QByteArray();
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error storing token in DB:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return QByteArray();
|
|
}
|
|
|
|
return token;
|
|
}
|
|
|
|
/*! Start the push button authentication for the device with the given \a deviceName. Returns the transaction id as refference to the request. */
|
|
int UserManager::requestPushButtonAuth(const QString &deviceName)
|
|
{
|
|
if (m_pushButtonTransaction.first != -1) {
|
|
qCWarning(dcUserManager()) << "PushButton authentication already in progress for device" << m_pushButtonTransaction.second << ". Cancelling...";
|
|
cancelPushButtonAuth(m_pushButtonTransaction.first);
|
|
}
|
|
|
|
qCDebug(dcUserManager()) << "Starting PushButton authentication for device" << deviceName;
|
|
int transactionId = ++m_pushButtonTransactionIdCounter;
|
|
m_pushButtonTransaction = QPair<int, QString>(transactionId, deviceName);
|
|
return transactionId;
|
|
}
|
|
|
|
/*! Cancel the push button authentication with the given \a transactionId.
|
|
|
|
\sa requestPushButtonAuth
|
|
*/
|
|
void UserManager::cancelPushButtonAuth(int transactionId)
|
|
{
|
|
if (m_pushButtonTransaction.first == -1) {
|
|
qCWarning(dcUserManager()) << "No PushButton transaction in progress. Nothing to cancel.";
|
|
return;
|
|
}
|
|
if (m_pushButtonTransaction.first != transactionId) {
|
|
qCWarning(dcUserManager()) << "PushButton transaction" << transactionId << "not in progress. Cannot cancel.";
|
|
return;
|
|
}
|
|
qCDebug(dcUserManager()) << "Cancelling PushButton transaction for device:" << m_pushButtonTransaction.second;
|
|
emit pushButtonAuthFinished(m_pushButtonTransaction.first, false, QByteArray());
|
|
m_pushButtonTransaction.first = -1;
|
|
|
|
}
|
|
|
|
/*! Request UserInfo.
|
|
The UserInfo for the given username is returned.
|
|
*/
|
|
UserInfo UserManager::userInfo(const QString &username) const
|
|
{
|
|
QString getUserQueryString = QString("SELECT * FROM users WHERE lower(username) = \"%1\";")
|
|
.arg(username);
|
|
|
|
QSqlQuery getUserQuery(m_db);
|
|
if (!getUserQuery.exec(getUserQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserInfo();
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for user" << username << "failed:" << getUserQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserInfo();
|
|
}
|
|
|
|
if (!getUserQuery.first())
|
|
return UserInfo();
|
|
|
|
UserInfo userInfo = UserInfo(getUserQuery.value("username").toString());
|
|
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;
|
|
}
|
|
|
|
QList<TokenInfo> UserManager::tokens(const QString &username) const
|
|
{
|
|
QList<TokenInfo> ret;
|
|
|
|
QSqlQuery query(m_db);
|
|
query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = ?;");
|
|
query.addBindValue(username.toLower());
|
|
query.exec();
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for tokens failed:" << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery();
|
|
return ret;
|
|
}
|
|
|
|
while (query.next()) {
|
|
ret << TokenInfo(query.value("id").toUuid(), query.value("username").toString(), query.value("creationdate").toDateTime(), query.value("devicename").toString());
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
TokenInfo UserManager::tokenInfo(const QByteArray &token) const
|
|
{
|
|
if (!validateToken(token)) {
|
|
qCWarning(dcUserManager) << "Token did not pass validation:" << token;
|
|
return TokenInfo();
|
|
}
|
|
|
|
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE token = \"%1\";")
|
|
.arg(QString::fromUtf8(token));
|
|
|
|
QSqlQuery getTokenQuery(m_db);
|
|
if (!getTokenQuery.exec(getTokenQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return TokenInfo();
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for token failed:" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return TokenInfo();
|
|
}
|
|
|
|
if (!getTokenQuery.first())
|
|
return TokenInfo();
|
|
|
|
return TokenInfo(getTokenQuery.value("id").toUuid(), getTokenQuery.value("username").toString(), getTokenQuery.value("creationdate").toDateTime(), getTokenQuery.value("devicename").toString());
|
|
}
|
|
|
|
TokenInfo UserManager::tokenInfo(const QUuid &tokenId) const
|
|
{
|
|
QString getTokenQueryString = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE id = \"%1\";")
|
|
.arg(tokenId.toString());
|
|
|
|
QSqlQuery getTokenQuery(m_db);
|
|
if (!getTokenQuery.exec(getTokenQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return TokenInfo();
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for token failed:" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return TokenInfo();
|
|
}
|
|
|
|
if (!getTokenQuery.first()) {
|
|
return TokenInfo();
|
|
}
|
|
return TokenInfo(getTokenQuery.value("id").toUuid(), getTokenQuery.value("username").toString(), getTokenQuery.value("creationdate").toDateTime(), getTokenQuery.value("devicename").toString());
|
|
}
|
|
|
|
/*! Removes the token with the given \a tokenId. Returns \l{UserError} to inform about the result. */
|
|
UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
|
|
{
|
|
QString removeTokenQueryString = QString("DELETE FROM tokens WHERE id = \"%1\";")
|
|
.arg(tokenId.toString());
|
|
|
|
QSqlQuery removeTokenQuery(m_db);
|
|
if (!removeTokenQuery.exec(removeTokenQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << removeTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Removing token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << removeTokenQueryString;
|
|
return UserErrorBackendError;
|
|
}
|
|
if (removeTokenQuery.numRowsAffected() != 1) {
|
|
qCWarning(dcUserManager) << "Token not found in DB";
|
|
return UserErrorTokenNotFound;
|
|
}
|
|
|
|
qCDebug(dcUserManager) << "Token" << tokenId << "removed from DB";
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
/*! Returns true, if the given \a token is valid. */
|
|
bool UserManager::verifyToken(const QByteArray &token)
|
|
{
|
|
if (!validateToken(token)) {
|
|
qCWarning(dcUserManager) << "Token failed character validation" << token;
|
|
return false;
|
|
}
|
|
QString getTokenQueryString = QString("SELECT * FROM tokens WHERE token = \"%1\";")
|
|
.arg(QString::fromUtf8(token));
|
|
|
|
QSqlQuery getTokenQuery(m_db);
|
|
if (!getTokenQuery.exec(getTokenQueryString)) {
|
|
qCWarning(dcUserManager()) << "Unable to execute SQL query" << getTokenQueryString << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return false;
|
|
}
|
|
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQueryString;
|
|
return false;
|
|
}
|
|
if (!getTokenQuery.first()) {
|
|
qCDebug(dcUserManager) << "Authorization failed for token" << token;
|
|
return false;
|
|
}
|
|
//qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
|
|
return true;
|
|
}
|
|
|
|
bool UserManager::restrictedThingAccess(const QByteArray &token) const
|
|
{
|
|
UserInfo ui = userInfo(tokenInfo(token).username());
|
|
return !ui.scopes().testFlag(Types::PermissionScopeAccessAllThings);
|
|
}
|
|
|
|
QList<ThingId> UserManager::allowedThingIds(const QByteArray &token) const
|
|
{
|
|
UserInfo ui = userInfo(tokenInfo(token).username());
|
|
return ui.allowedThingIds();
|
|
}
|
|
|
|
bool UserManager::initDB()
|
|
{
|
|
m_db.close();
|
|
|
|
if (!m_db.open()) {
|
|
dumpDBError("Can't open user database. Init failed.");
|
|
return false;
|
|
}
|
|
|
|
int currentVersion = -1;
|
|
int newVersion = 2;
|
|
|
|
if (m_db.tables().contains("metadata")) {
|
|
QSqlQuery query(m_db);
|
|
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()) << "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, 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).");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
// Migrated existing users from before multiuser support are admins by default
|
|
query = QSqlQuery(m_db);
|
|
query.prepare("UPDATE users SET scopes = ?;");
|
|
query.addBindValue(Types::scopesToStringList(Types::PermissionScopeAdmin).join(','));
|
|
|
|
if (!query.exec() || query.lastError().isValid()) {
|
|
dumpDBError("Error migrating user database (updating existing users).");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
query = QSqlQuery(m_db);
|
|
if (!query.exec("ALTER TABLE users ADD COLUMN email VARCHAR(40);") || m_db.lastError().isValid()) {
|
|
dumpDBError("Error migrating user database (table users).");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
query = QSqlQuery(m_db);
|
|
if (!query.exec("ALTER TABLE users ADD COLUMN displayName VARCHAR(40);") || m_db.lastError().isValid()) {
|
|
dumpDBError("Error migrating user database (table users).");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
// Up until schema 1, username was an email. Copy it to initialize the email field.
|
|
query = QSqlQuery(m_db);
|
|
if (!query.exec("UPDATE users SET email = username;") || m_db.lastError().isValid()) {
|
|
dumpDBError("Error migrating user database (table users).");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
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()) << "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)");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
dumpDBError("Error setting up user database (table metadata)!");
|
|
m_db.close();
|
|
return false;
|
|
}
|
|
|
|
query = QSqlQuery(m_db);
|
|
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;
|
|
}
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
|
|
// Migration from before 1.0:
|
|
// Push button tokens were given out without an explicit user name
|
|
// If we have push button tokens (userId "") but no explicit user, let's create it as admin
|
|
// Users without valid username will have password login disabled.
|
|
QSqlQuery query(m_db);
|
|
query.prepare("SELECT * FROM tokens WHERE username = \"\";");
|
|
query.exec();
|
|
if (query.lastError().type() == QSqlError::NoError && query.next()) {
|
|
QSqlQuery query(m_db);
|
|
query.prepare("SELECT * FROM users WHERE username = \"\";");
|
|
query.exec();
|
|
if (!query.next()) {
|
|
qCDebug(dcUserManager()) << "Tokens existing but no user. Creating token admin user";
|
|
QSqlQuery query(m_db);
|
|
query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) values(?, ?, ?, ?, ?, ?);");
|
|
query.addBindValue("");
|
|
query.addBindValue("");
|
|
query.addBindValue("Admin");
|
|
query.addBindValue("");
|
|
query.addBindValue("");
|
|
query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin));
|
|
query.exec();
|
|
if (query.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText();
|
|
}
|
|
}
|
|
}
|
|
|
|
qCDebug(dcUserManager()) << "User database initialized successfully";
|
|
return true;
|
|
}
|
|
|
|
void UserManager::rotate(const QString &dbName)
|
|
{
|
|
int index = 1;
|
|
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))) {
|
|
qCWarning(dcUserManager()) << "Error backing up old database.";
|
|
} else {
|
|
qCDebug(dcUserManager()) << "Successfully moved old database";
|
|
}
|
|
}
|
|
|
|
bool UserManager::validateUsername(const QString &username) const
|
|
{
|
|
QRegularExpression validator("[a-zA-Z0-9_\\.+-@]{3,}");
|
|
return validator.match(username).hasMatch();
|
|
}
|
|
|
|
bool UserManager::validatePassword(const QString &password) const
|
|
{
|
|
if (password.length() < 8) {
|
|
return false;
|
|
}
|
|
if (!password.contains(QRegularExpression("[a-z]"))) {
|
|
return false;
|
|
}
|
|
if (!password.contains(QRegularExpression("[A-Z]"))) {
|
|
return false;
|
|
}
|
|
if (!password.contains(QRegularExpression("[0-9]"))) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UserManager::validateToken(const QByteArray &token) const
|
|
{
|
|
QRegularExpression validator(QRegularExpression("(^[a-zA-Z0-9_\\.+-/=]+$)"));
|
|
return validator.match(token).hasMatch();
|
|
}
|
|
|
|
bool UserManager::validateScopes(Types::PermissionScopes scopes) const
|
|
{
|
|
if (scopes.testFlag(Types::PermissionScopeAdmin) || scopes == Types::PermissionScopeNone || scopes == Types::PermissionScopeControlThings)
|
|
return true;
|
|
|
|
if (scopes.testFlag(Types::PermissionScopeConfigureThings)) {
|
|
if (!scopes.testFlag(Types::PermissionScopeControlThings) ||
|
|
!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
|
|
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can configure things he must have access to all things and must be able to control them.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Note: if access to all things, there are no restrictions
|
|
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
|
|
if (scopes.testFlag(Types::PermissionScopeControlThings) ||
|
|
scopes.testFlag(Types::PermissionScopeConfigureRules)||
|
|
scopes.testFlag(Types::PermissionScopeExecuteRules)) {
|
|
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user has not access to all things, he can not configure them or create/execute rules.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (scopes.testFlag(Types::PermissionScopeExecuteRules)) {
|
|
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings)) {
|
|
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can execute rules, he must have access to all things.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (scopes.testFlag(Types::PermissionScopeConfigureRules)) {
|
|
if (!scopes.testFlag(Types::PermissionScopeAccessAllThings) ||
|
|
!scopes.testFlag(Types::PermissionScopeExecuteRules)) {
|
|
qCWarning(dcUserManager()) << "Invalid scopes combination. If a user can create rules, he must have access to all things and be able to execute them.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void UserManager::dumpDBError(const QString &message)
|
|
{
|
|
qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
|
}
|
|
|
|
void UserManager::onPushButtonPressed()
|
|
{
|
|
if (m_pushButtonTransaction.first == -1) {
|
|
qCDebug(dcUserManager()) << "PushButton pressed without a client waiting for it. Ignoring the signal.";
|
|
return;
|
|
}
|
|
|
|
// Creating a user without username and password. It won't be able to log in via user/password
|
|
QSqlQuery query(m_db);
|
|
query.prepare("SELECT * FROM users WHERE username = \"\";");
|
|
query.exec();
|
|
if (!query.next()) {
|
|
qCDebug(dcUserManager()) << "Creating token admin user";
|
|
QSqlQuery query(m_db);
|
|
query.prepare("INSERT INTO users(username, password, salt, scopes) values(?, ?, ?, ?);");
|
|
query.addBindValue("");
|
|
query.addBindValue("");
|
|
query.addBindValue("");
|
|
query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin));
|
|
query.exec();
|
|
if (query.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText();
|
|
}
|
|
}
|
|
|
|
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
|
|
QString storeTokenQueryString = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
|
|
.arg(QUuid::createUuid().toString())
|
|
.arg("")
|
|
.arg(QString::fromUtf8(token))
|
|
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
|
|
.arg(m_pushButtonTransaction.second);
|
|
|
|
QSqlQuery storeTokenQuery(m_db);
|
|
if (!storeTokenQuery.exec(storeTokenQueryString) || m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager()) << "Error storing token in DB:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
qCWarning(dcUserManager()) << "PushButton Auth failed.";
|
|
emit pushButtonAuthFinished(m_pushButtonTransaction.first, false, QByteArray());
|
|
} else {
|
|
qCDebug(dcUserManager()) << "PushButton Auth succeeded.";
|
|
emit pushButtonAuthFinished(m_pushButtonTransaction.first, true, token);
|
|
}
|
|
|
|
m_pushButtonTransaction.first = -1;
|
|
}
|
|
|
|
}
|