mirror of https://github.com/nymea/nymea.git
315 lines
13 KiB
C++
315 lines
13 KiB
C++
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* *
|
|
* Copyright (C) 2017 Michael Zanetti <michael.zanetti@guh.io> *
|
|
* *
|
|
* This file is part of guh. *
|
|
* *
|
|
* Guh is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, version 2 of the License. *
|
|
* *
|
|
* Guh 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 General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with guh. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#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 <QRegExpValidator>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
|
|
namespace guhserver {
|
|
|
|
UserManager::UserManager(QObject *parent) : QObject(parent)
|
|
{
|
|
m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), "users");
|
|
m_db.setDatabaseName(NymeaSettings::settingsPath() + "/user-db.sqlite");
|
|
|
|
if (!m_db.open()) {
|
|
qCWarning(dcUserManager) << "Error opening users database:" << m_db.lastError().driverText();
|
|
return;
|
|
}
|
|
initDB();
|
|
|
|
m_pushButtonDBusService = new PushButtonDBusService("/io/guh/nymead/UserManager", this);
|
|
connect(m_pushButtonDBusService, &PushButtonDBusService::pushButtonPressed, this, &UserManager::onPushButtonPressed);
|
|
m_pushButtonTransaction = qMakePair<int, QString>(-1, QString());
|
|
}
|
|
|
|
QStringList UserManager::users() const
|
|
{
|
|
QString userQuery("SELECT username FROM users;");
|
|
QSqlQuery result = m_db.exec(userQuery);
|
|
QStringList ret;
|
|
while (result.next()) {
|
|
ret << result.value("username").toString();
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
UserManager::UserError UserManager::createUser(const QString &username, const QString &password)
|
|
{
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Error creating user. Invalid username";
|
|
return UserErrorInvalidUserId;
|
|
}
|
|
|
|
QRegExp passwordValidator = QRegExp("^(?=.*[A-Za-z])(?=.*\[0-9])(?=.*[$@$!%*#?&])[A-Za-z0-9$@$!%*#?&]{8,}$");
|
|
if (!passwordValidator.exactMatch(password)) {
|
|
qCWarning(dcUserManager) << "Password failed character validation. Must contain a letter, a number and a special charactar. Minimum length: 8";
|
|
return UserErrorBadPassword;
|
|
}
|
|
|
|
QString checkForDuplicateUserQuery = QString("SELECT * FROM users WHERE lower(username) = \"%1\";").arg(username.toLower());
|
|
QSqlQuery result = m_db.exec(checkForDuplicateUserQuery);
|
|
if (result.first()) {
|
|
qCWarning(dcUserManager) << "Username already in use";
|
|
return UserErrorDuplicateUserId;
|
|
}
|
|
|
|
QByteArray salt = QUuid::createUuid().toString().remove(QRegExp("[{}]")).toUtf8();
|
|
QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64();
|
|
QString queryString = QString("INSERT INTO users(username, password, salt) values(\"%1\", \"%2\", \"%3\");")
|
|
.arg(username)
|
|
.arg(QString::fromUtf8(hashedPassword))
|
|
.arg(QString::fromUtf8(salt));
|
|
m_db.exec(queryString);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error creating user:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
return UserErrorBackendError;
|
|
}
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
UserManager::UserError UserManager::removeUser(const QString &username)
|
|
{
|
|
QString dropUserQuery = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower());
|
|
QSqlQuery result = m_db.exec(dropUserQuery);
|
|
if (result.numRowsAffected() == 0) {
|
|
return UserErrorInvalidUserId;
|
|
}
|
|
|
|
QString dropTokensQuery = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower());
|
|
m_db.exec(dropTokensQuery);
|
|
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
bool UserManager::pushButtonAuthAvailable() const
|
|
{
|
|
return m_pushButtonDBusService->agentAvailable();
|
|
}
|
|
|
|
QByteArray UserManager::authenticate(const QString &username, const QString &password, const QString &deviceName)
|
|
{
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Username did not pass validation:" << username;
|
|
return QByteArray();
|
|
}
|
|
|
|
QString passwordQuery = QString("SELECT password, salt FROM users WHERE lower(username) = \"%1\";").arg(username.toLower());
|
|
QSqlQuery result = m_db.exec(passwordQuery);
|
|
if (!result.first()) {
|
|
qCWarning(dcUserManager) << "No such username" << username;
|
|
return QByteArray();
|
|
}
|
|
QByteArray salt = result.value("salt").toByteArray();
|
|
QByteArray hashedPassword = result.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 storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");")
|
|
.arg(QUuid::createUuid().toString())
|
|
.arg(username)
|
|
.arg(QString::fromUtf8(token))
|
|
.arg(NymeaCore::instance()->timeManager()->currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
|
|
.arg(deviceName);
|
|
|
|
m_db.exec(storeTokenQuery);
|
|
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;
|
|
}
|
|
|
|
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 = qMakePair<int, QString>(transactionId, deviceName);
|
|
return transactionId;
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
QString UserManager::userForToken(const QByteArray &token) const
|
|
{
|
|
if (!validateToken(token)) {
|
|
qCWarning(dcUserManager) << "Token failed character validation:" << token;
|
|
return QString();
|
|
}
|
|
QString getUserQuery = QString("SELECT * FROM tokens WHERE token = \"%1\";")
|
|
.arg(QString::fromUtf8(token));
|
|
QSqlQuery result = m_db.exec(getUserQuery);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error fetching username for token:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery;
|
|
return QString();
|
|
}
|
|
if (!result.first()) {
|
|
qCWarning(dcUserManager) << "No such token in DB:" << token;
|
|
return QString();
|
|
}
|
|
|
|
return result.value("username").toString();
|
|
}
|
|
|
|
QList<TokenInfo> UserManager::tokens(const QString &username) const
|
|
{
|
|
QList<TokenInfo> ret;
|
|
|
|
if (!validateUsername(username)) {
|
|
qCWarning(dcUserManager) << "Username did not pass validation:" << username;
|
|
return ret;
|
|
}
|
|
QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = \"%1\";")
|
|
.arg(username.toLower());
|
|
QSqlQuery result = m_db.exec(getTokensQuery);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery;
|
|
return ret;
|
|
}
|
|
|
|
while (result.next()) {
|
|
ret << TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString());
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
UserManager::UserError UserManager::removeToken(const QUuid &tokenId)
|
|
{
|
|
QString removeTokenQuery = QString("DELETE FROM tokens WHERE id = \"%1\";")
|
|
.arg(tokenId.toString());
|
|
QSqlQuery result = m_db.exec(removeTokenQuery);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Removing token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << removeTokenQuery;
|
|
return UserErrorBackendError;
|
|
}
|
|
if (result.numRowsAffected() != 1) {
|
|
qCWarning(dcUserManager) << "Token not found in DB";
|
|
return UserErrorTokenNotFound;
|
|
}
|
|
|
|
qCDebug(dcUserManager) << "Token" << tokenId << "removed from DB";
|
|
return UserErrorNoError;
|
|
}
|
|
|
|
bool UserManager::verifyToken(const QByteArray &token)
|
|
{
|
|
if (!validateToken(token)) {
|
|
qCWarning(dcUserManager) << "Token failed character validation" << token;
|
|
return false;
|
|
}
|
|
QString getTokenQuery = QString("SELECT * FROM tokens WHERE token = \"%1\";")
|
|
.arg(QString::fromUtf8(token));
|
|
QSqlQuery result = m_db.exec(getTokenQuery);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokenQuery;
|
|
return false;
|
|
}
|
|
if (!result.first()) {
|
|
qCDebug(dcUserManager) << "Authorisation failed for token" << token;
|
|
return false;
|
|
}
|
|
//qCDebug(dcUserManager) << "Token authorized for user" << result.value("username").toString();
|
|
return true;
|
|
}
|
|
|
|
void UserManager::initDB()
|
|
{
|
|
if (!m_db.tables().contains("users")) {
|
|
m_db.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE, password VARCHAR(100), salt VARCHAR(100));");
|
|
}
|
|
if (!m_db.tables().contains("tokens")) {
|
|
m_db.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));");
|
|
}
|
|
}
|
|
|
|
bool UserManager::validateUsername(const QString &username) const
|
|
{
|
|
QRegExp validator("(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$)");
|
|
return validator.exactMatch(username);
|
|
}
|
|
|
|
bool UserManager::validateToken(const QByteArray &token) const
|
|
{
|
|
QRegExp validator(QRegExp("(^[a-zA-Z0-9_.+-/=]+$)"));
|
|
return validator.exactMatch(token);
|
|
}
|
|
|
|
void UserManager::onPushButtonPressed()
|
|
{
|
|
if (m_pushButtonTransaction.first == -1) {
|
|
qCDebug(dcUserManager()) << "PushButton pressed but don't have a transaction waiting for it.";
|
|
return;
|
|
}
|
|
|
|
QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64();
|
|
QString storeTokenQuery = 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);
|
|
|
|
m_db.exec(storeTokenQuery);
|
|
if (m_db.lastError().type() != QSqlError::NoError) {
|
|
qCWarning(dcUserManager) << "Error storing token in DB:" << m_db.lastError().databaseText() << m_db.lastError().driverText();
|
|
emit pushButtonAuthFinished(m_pushButtonTransaction.first, false, QByteArray());
|
|
}
|
|
qCDebug(dcUserManager()) << "PushButton Auth succeeded";
|
|
emit pushButtonAuthFinished(m_pushButtonTransaction.first, true, token);
|
|
|
|
m_pushButtonTransaction.first = -1;
|
|
}
|
|
|
|
}
|