mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[Server/Client/Protocol] Reporting users + moderation queue functionality (#7091)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [Server/Client/Protocol] Reporting users + moderation queue functionality Took 6 minutes Took 3 minutes Took 8 seconds Took 11 minutes Took 12 minutes Took 7 minutes Took 15 seconds Took 2 minutes Took 1 minute Took 30 seconds Took 16 seconds * CI Fix Took 6 minutes * CI Fix Took 6 minutes * [Protocol] Add moderation investigation commands Adds the protocol layer for the moderation investigation suite: - Command_GetUserSessions/GetUserAlts/GetModeratorLastLogins/ResetUserPassword/RemoveUserAvatar (1013-1017) - Response extensions 1215-1219 with ServerInfo messages for sessions, alts, and staff logins - last_login on Response_ReportUserInfo and warning_il on Response_WarnList Took 2 minutes * [Utility] Add warning categories parser with infraction levels Parses the server's 'officialwarnings' setting (comma-separated, optional '|IL' suffix) into WarningCategory structs so the client can display the infraction level of each warning category. Includes GTest coverage. * [Server] Add moderation investigation tools Implements the server side of the moderation suite: - getUserSessions/getUserAlts/getModeratorLastLogins/removeUserAvatar DB methods - Handlers for all five new commands with audit records (PASSWORD_RESET, REMOVE_USER_AVATAR); password resets return a generated temporary password - cmdGetWarnList now reports per-category infraction levels from the officialwarnings setting; cmdReportUserInfo reports last_login - Update servatrice.ini.example with the warning taxonomy - Password/avatar mutations report RespNameNotFound when the user does not exist * [Client] Add moderation tab with investigate, password reset, and avatar removal - New Moderation tab: search a user to show account info, alternate accounts, login sessions, and staff last logins; actions to reset the user's password (shows the generated temporary password) and remove the user's avatar - 'Investigate user' entry in the user context menu opens the tab pre-loaded for that user - Warning dialog shows the infraction level of each warning category - Tab wired into TabSupervisor with a moderator-gated menu action, shortcut, and tabs.ini persistence (default closed) * [Server/Client/Protocol] Address PR #7091 review: security, bug, and perf fixes Security: - Promote RESET_USER_PASSWORD to admin-only dispatch (was moderator-accessible) - Reject password reset on users with equal/higher privilege than caller - Notify affected user via Event_NotifyUser::CUSTOM when password is reset - Add server-side category whitelist for reports - Drop reporter name fallback in comment/details authorization (ID-only) - Force password change: new DB column + login enforcement + client disconnect Bugs: - XSS via QTextEdit::append() → insertPlainText() in report tab and utils - allNotified initialized to true even with empty recipients list - Warning combo box: use currentData() instead of baked-in display text - Report resolution now records who resolved (resolved_by column + audit) Performance: - IP-correlation subquery: add 6-month window + LIMIT 200 - getUserSessions: clamp limit to 500 Non-blocking: - Palette-aware colors in report_utils.cpp (dark/light mode) - Report list pagination: offset/limit fields + total_count in response - SessionCommand enum gap comment for reserved values 1201-1203 Schema: 36→37 (force_password_change), 37→38 (resolved_by) Took 12 minutes Took 16 seconds * Fix macOs pedantry Took 5 minutes --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
9eafd90a91
commit
ed4eb1cb31
83 changed files with 4768 additions and 43 deletions
|
|
@ -14,6 +14,7 @@
|
|||
#include <QSqlQuery>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/utility/passwordhasher.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface");
|
||||
|
|
@ -339,8 +340,8 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
|
|||
return UserIsBanned;
|
||||
}
|
||||
|
||||
QSqlQuery *passwordQuery =
|
||||
prepareQuery("select password_sha512, active from {prefix}_users where name = :name");
|
||||
QSqlQuery *passwordQuery = prepareQuery(
|
||||
"select password_sha512, active, force_password_change from {prefix}_users where name = :name");
|
||||
passwordQuery->bindValue(":name", user);
|
||||
if (!execSqlQuery(passwordQuery)) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error";
|
||||
|
|
@ -350,6 +351,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
|
|||
if (passwordQuery->next()) {
|
||||
const QString correctPasswordSha512 = passwordQuery->value(0).toString();
|
||||
const bool userIsActive = passwordQuery->value(1).toBool();
|
||||
const bool forceChange = passwordQuery->value(2).toBool();
|
||||
if (!userIsActive) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Login denied: user not active";
|
||||
return UserIsInactive;
|
||||
|
|
@ -361,6 +363,10 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
|
|||
hashedPassword = password;
|
||||
}
|
||||
if (correctPasswordSha512 == hashedPassword) {
|
||||
if (forceChange) {
|
||||
qCDebug(DatabaseInterfaceLog) << "Login accepted but password change required";
|
||||
return PasswordChangeRequired;
|
||||
}
|
||||
qCDebug(DatabaseInterfaceLog) << "Login accepted: password right";
|
||||
return PasswordRight;
|
||||
} else {
|
||||
|
|
@ -1084,11 +1090,22 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user,
|
|||
"passwordLastChangedDate = NOW() where name = :name");
|
||||
passwordQuery->bindValue(":password", passwordSha512);
|
||||
passwordQuery->bindValue(":name", user);
|
||||
if (execSqlQuery(passwordQuery)) {
|
||||
return true;
|
||||
if (!execSqlQuery(passwordQuery)) {
|
||||
return false;
|
||||
}
|
||||
return passwordQuery->numRowsAffected() > 0;
|
||||
}
|
||||
|
||||
void Servatrice_DatabaseInterface::setForcePasswordChange(const QString &user, bool force)
|
||||
{
|
||||
if (!checkSql()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return false;
|
||||
QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET force_password_change = :force WHERE name = :name");
|
||||
query->bindValue(":force", force ? 1 : 0);
|
||||
query->bindValue(":name", user);
|
||||
execSqlQuery(query);
|
||||
}
|
||||
|
||||
bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user,
|
||||
|
|
@ -1314,6 +1331,169 @@ QList<ServerInfo_Warning> Servatrice_DatabaseInterface::getUserWarnHistory(const
|
|||
return results;
|
||||
}
|
||||
|
||||
QList<ServerInfo_UserSession> Servatrice_DatabaseInterface::getUserSessions(const QString &userName, int limit)
|
||||
{
|
||||
QList<ServerInfo_UserSession> results;
|
||||
|
||||
if (!checkSql()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
QSqlQuery *query = prepareQuery("SELECT user_name, ip_address, clientid, "
|
||||
"UNIX_TIMESTAMP(start_time), UNIX_TIMESTAMP(end_time), connection_type "
|
||||
"FROM {prefix}_sessions WHERE user_name = :user_name "
|
||||
"ORDER BY start_time DESC LIMIT :limit");
|
||||
query->bindValue(":user_name", userName);
|
||||
query->bindValue(":limit", limit);
|
||||
|
||||
if (!execSqlQuery(query)) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Failed to collect session history information: SQL Error";
|
||||
return results;
|
||||
}
|
||||
|
||||
while (query->next()) {
|
||||
ServerInfo_UserSession sessionDetails;
|
||||
sessionDetails.set_user_name(query->value(0).toString().toStdString());
|
||||
sessionDetails.set_ip_address(query->value(1).toString().toStdString());
|
||||
sessionDetails.set_clientid(query->value(2).toString().toStdString());
|
||||
sessionDetails.set_start_time(query->value(3).toLongLong());
|
||||
if (!query->value(4).isNull()) {
|
||||
sessionDetails.set_end_time(query->value(4).toLongLong());
|
||||
}
|
||||
sessionDetails.set_connection_type(query->value(5).toString().toStdString());
|
||||
results << sessionDetails;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QString &userName)
|
||||
{
|
||||
QList<ServerInfo_UserAlt> results;
|
||||
|
||||
if (!checkSql()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Seed account identifiers used to find related accounts
|
||||
QSqlQuery *seedQuery = prepareQuery("SELECT email, clientid FROM {prefix}_users WHERE name = :user_name");
|
||||
seedQuery->bindValue(":user_name", userName);
|
||||
if (!execSqlQuery(seedQuery) || !seedQuery->next()) {
|
||||
return results;
|
||||
}
|
||||
const QString seedEmail = seedQuery->value(0).toString();
|
||||
const QString seedClientId = seedQuery->value(1).toString();
|
||||
|
||||
QString queryString = "SELECT u.name, u.email, u.clientid, UNIX_TIMESTAMP(u.registrationDate), "
|
||||
"UNIX_TIMESTAMP(a.last_login), "
|
||||
"(SELECT COUNT(*) FROM {prefix}_warnings w WHERE w.user_id = u.id), "
|
||||
"(SELECT COUNT(*) FROM {prefix}_bans b WHERE b.user_name = u.name), "
|
||||
"u.active "
|
||||
"FROM {prefix}_users u "
|
||||
"LEFT JOIN {prefix}_user_analytics a ON a.id = u.id "
|
||||
"WHERE u.name = :user_name";
|
||||
if (!seedEmail.isEmpty()) {
|
||||
queryString.append(" OR u.email = :seed_email");
|
||||
}
|
||||
if (!seedClientId.isEmpty()) {
|
||||
queryString.append(" OR u.clientid = :seed_clientid");
|
||||
}
|
||||
queryString.append(" OR u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s "
|
||||
"WHERE s.ip_address IN (SELECT DISTINCT s2.ip_address FROM {prefix}_sessions s2 "
|
||||
"WHERE s2.user_name = :user_name"
|
||||
" AND s2.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH))"
|
||||
" AND s.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH)) "
|
||||
"ORDER BY u.name LIMIT 200");
|
||||
|
||||
QSqlQuery *query = prepareQuery(queryString);
|
||||
query->bindValue(":user_name", userName);
|
||||
if (!seedEmail.isEmpty()) {
|
||||
query->bindValue(":seed_email", seedEmail);
|
||||
}
|
||||
if (!seedClientId.isEmpty()) {
|
||||
query->bindValue(":seed_clientid", seedClientId);
|
||||
}
|
||||
|
||||
if (!execSqlQuery(query)) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Failed to collect user alt information: SQL Error";
|
||||
return results;
|
||||
}
|
||||
|
||||
while (query->next()) {
|
||||
ServerInfo_UserAlt altDetails;
|
||||
altDetails.set_user_name(query->value(0).toString().toStdString());
|
||||
altDetails.set_email(query->value(1).toString().toStdString());
|
||||
altDetails.set_clientid(query->value(2).toString().toStdString());
|
||||
altDetails.set_registration_time(query->value(3).toLongLong());
|
||||
if (!query->value(4).isNull()) {
|
||||
altDetails.set_last_login(query->value(4).toLongLong());
|
||||
}
|
||||
altDetails.set_warn_count(query->value(5).toInt());
|
||||
altDetails.set_ban_count(query->value(6).toInt());
|
||||
altDetails.set_is_active(query->value(7).toBool());
|
||||
results << altDetails;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<ServerInfo_ModeratorLogin> Servatrice_DatabaseInterface::getModeratorLastLogins()
|
||||
{
|
||||
QList<ServerInfo_ModeratorLogin> results;
|
||||
|
||||
if (!checkSql()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
QSqlQuery *query = prepareQuery("SELECT u.name, u.admin, UNIX_TIMESTAMP(a.last_login) "
|
||||
"FROM {prefix}_users u "
|
||||
"LEFT JOIN {prefix}_user_analytics a ON a.id = u.id "
|
||||
"WHERE (u.admin & 7) <> 0 ORDER BY u.name");
|
||||
|
||||
if (!execSqlQuery(query)) {
|
||||
qCWarning(DatabaseInterfaceLog) << "Failed to collect moderator login information: SQL Error";
|
||||
return results;
|
||||
}
|
||||
|
||||
while (query->next()) {
|
||||
ServerInfo_ModeratorLogin loginDetails;
|
||||
loginDetails.set_user_name(query->value(0).toString().toStdString());
|
||||
|
||||
const int isAdmin = query->value(1).toInt();
|
||||
int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered;
|
||||
if (isAdmin & 1) {
|
||||
userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator;
|
||||
} else if (isAdmin & 2) {
|
||||
userLevel |= ServerInfo_User::IsModerator;
|
||||
}
|
||||
if (isAdmin & 4) {
|
||||
userLevel |= ServerInfo_User::IsJudge;
|
||||
}
|
||||
loginDetails.set_user_level(userLevel);
|
||||
|
||||
if (!query->value(2).isNull()) {
|
||||
loginDetails.set_last_login(query->value(2).toLongLong());
|
||||
}
|
||||
results << loginDetails;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName)
|
||||
{
|
||||
if (!checkSql()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET avatar_bmp = '' WHERE name = :user_name");
|
||||
query->bindValue(":user_name", userName);
|
||||
if (!execSqlQuery(query)) {
|
||||
return false;
|
||||
}
|
||||
return query->numRowsAffected() > 0;
|
||||
}
|
||||
|
||||
QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user,
|
||||
const QString &ipaddress,
|
||||
const QString &gamename,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue