From 839c717272475bcecf7885b81a4e1eca1cd059f1 Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+Steals-The-PRs@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:35:34 +0300 Subject: [PATCH] [MIRROR] Refactored admin backup saving. No longer at round end, more data backed up (#1758) (#2708) * Refactored admin backup saving. No longer at round end, more data backed up (#81891) Admin verified connections now cache all verified connections for all admins. (Rather then just the last connection data of the currently connected admins) Sync with the db now happens at admin load time, not at round end. (this was causing annoyances because servers with long rounds could override the admin db with old/stale data overwritting the fresher data that was written by a server with a shorter round) Fix backup verification not working if the db thinks it still connected but its not actually still connected. @Mothblocks @Jordie0608 --------- * Refactored admin backup saving. No longer at round end, more data backed up --------- Co-authored-by: NovaBot <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: Kyle Spier-Swenson Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> --- code/__HELPERS/roundend.dm | 80 ---------------- code/controllers/subsystem/ticker.dm | 2 - code/modules/admin/admin_ranks.dm | 137 +++++++++++++++++++++++---- code/modules/admin/holder2.dm | 72 +++++++++++--- 4 files changed, 175 insertions(+), 116 deletions(-) diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index d8cff863625..3ea75be832a 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -742,86 +742,6 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) count++ return objective_parts.Join("
") -/datum/controller/subsystem/ticker/proc/save_admin_data() - if(IsAdminAdvancedProcCall()) - to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.") - return - if(CONFIG_GET(flag/admin_legacy_system)) //we're already using legacy system so there's nothing to save - return - else if(load_admins(TRUE)) //returns true if there was a database failure and the backup was loaded from - return - sync_ranks_with_db() - var/list/sql_admins = list() - for(var/i in GLOB.protected_admins) - var/datum/admins/A = GLOB.protected_admins[i] - sql_admins += list(list("ckey" = A.target, "rank" = A.rank_names())) - SSdbcore.MassInsert(format_table_name("admin"), sql_admins, duplicate_key = TRUE) - var/datum/db_query/query_admin_rank_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] p INNER JOIN [format_table_name("admin")] a ON p.ckey = a.ckey SET p.lastadminrank = a.rank") - query_admin_rank_update.Execute() - qdel(query_admin_rank_update) - - //json format backup file generation stored per server - var/json_file = file("data/admins_backup.json") - var/list/file_data = list( - "ranks" = list(), - "admins" = list(), - "connections" = list(), - ) - for(var/datum/admin_rank/R in GLOB.admin_ranks) - file_data["ranks"]["[R.name]"] = list() - file_data["ranks"]["[R.name]"]["include rights"] = R.include_rights - file_data["ranks"]["[R.name]"]["exclude rights"] = R.exclude_rights - file_data["ranks"]["[R.name]"]["can edit rights"] = R.can_edit_rights - - for(var/admin_ckey in GLOB.admin_datums + GLOB.deadmins) - var/datum/admins/admin = GLOB.admin_datums[admin_ckey] - - if(!admin) - admin = GLOB.deadmins[admin_ckey] - if (!admin) - continue - - file_data["admins"][admin_ckey] = admin.rank_names() - - if (admin.owner) - file_data["connections"][admin_ckey] = list( - "cid" = admin.owner.computer_id, - "ip" = admin.owner.address, - ) - - fdel(json_file) - WRITE_FILE(json_file, json_encode(file_data)) - -/datum/controller/subsystem/ticker/proc/update_everything_flag_in_db() - for(var/datum/admin_rank/R in GLOB.admin_ranks) - var/list/flags = list() - if(R.include_rights == R_EVERYTHING) - flags += "flags" - if(R.exclude_rights == R_EVERYTHING) - flags += "exclude_flags" - if(R.can_edit_rights == R_EVERYTHING) - flags += "can_edit_flags" - if(!flags.len) - continue - var/flags_to_check = flags.Join(" != [R_EVERYTHING] AND ") + " != [R_EVERYTHING]" - var/datum/db_query/query_check_everything_ranks = SSdbcore.NewQuery( - "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = :rank AND ([flags_to_check])", - list("rank" = R.name) - ) - if(!query_check_everything_ranks.Execute()) - qdel(query_check_everything_ranks) - return - if(query_check_everything_ranks.NextRow()) //no row is returned if the rank already has the correct flag value - var/flags_to_update = flags.Join(" = [R_EVERYTHING], ") + " = [R_EVERYTHING]" - var/datum/db_query/query_update_everything_ranks = SSdbcore.NewQuery( - "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = :rank", - list("rank" = R.name) - ) - if(!query_update_everything_ranks.Execute()) - qdel(query_update_everything_ranks) - return - qdel(query_update_everything_ranks) - qdel(query_check_everything_ranks) /datum/controller/subsystem/ticker/proc/cheevo_report() var/list/parts = list() diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index f1ca584bfe5..22fc7ba29cd 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -765,8 +765,6 @@ SUBSYSTEM_DEF(ticker) /datum/controller/subsystem/ticker/Shutdown() gather_newscaster() //called here so we ensure the log is created even upon admin reboot - save_admin_data() - update_everything_flag_in_db() if(!round_end_sound) round_end_sound = choose_round_end_song() ///The reference to the end of round sound that we have chosen. diff --git a/code/modules/admin/admin_ranks.dm b/code/modules/admin/admin_ranks.dm index c39464e3ef0..3b959d449cd 100644 --- a/code/modules/admin/admin_ranks.dm +++ b/code/modules/admin/admin_ranks.dm @@ -102,19 +102,8 @@ GLOBAL_PROTECT(protected_ranks) if(3) can_edit_rights |= flag -/proc/sync_ranks_with_db() - set waitfor = FALSE - - if(IsAdminAdvancedProcCall()) - to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.", confidential = TRUE) - return - - var/list/sql_ranks = list() - for(var/datum/admin_rank/R in GLOB.protected_ranks) - sql_ranks += list(list("rank" = R.name, "flags" = R.include_rights, "exclude_flags" = R.exclude_rights, "can_edit_flags" = R.can_edit_rights)) - SSdbcore.MassInsert(format_table_name("admin_ranks"), sql_ranks, duplicate_key = TRUE) - -//load our rank - > rights associations +/// Loads admin ranks. +/// Return a list containing the backup data if they were loaded from the database backup json /proc/load_admin_ranks(dbfail, no_update) if(IsAdminAdvancedProcCall()) to_chat(usr, "Admin Reload blocked: Advanced ProcCall detected.", confidential = TRUE) @@ -137,7 +126,7 @@ GLOBAL_PROTECT(protected_ranks) GLOB.admin_ranks += R GLOB.protected_ranks += R previous_rank = R - if(!CONFIG_GET(flag/admin_legacy_system) || dbfail) + if(!CONFIG_GET(flag/admin_legacy_system) && !dbfail) if(CONFIG_GET(flag/load_legacy_ranks_only)) if(!no_update) sync_ranks_with_db() @@ -146,7 +135,7 @@ GLOBAL_PROTECT(protected_ranks) if(!query_load_admin_ranks.Execute()) message_admins("Error loading admin ranks from database. Loading from backup.") log_sql("Error loading admin ranks from database. Loading from backup.") - dbfail = 1 + dbfail = TRUE else while(query_load_admin_ranks.NextRow()) var/skip @@ -220,12 +209,14 @@ GLOBAL_PROTECT(protected_ranks) return jointext(names, "+") +/// (Re)Loads the admin list. +/// returns TRUE if database admins had to be loaded from the backup json /proc/load_admins(no_update) var/dbfail if(!CONFIG_GET(flag/admin_legacy_system) && !SSdbcore.Connect()) message_admins("Failed to connect to database while loading admins. Loading from backup.") log_sql("Failed to connect to database while loading admins. Loading from backup.") - dbfail = 1 + dbfail = TRUE //clear the datums references GLOB.admin_datums.Cut() for(var/client/C in GLOB.admins) @@ -251,7 +242,7 @@ GLOBAL_PROTECT(protected_ranks) var/admin_rank = admins_regex.group[2] new /datum/admins(ranks_from_rank_name(admin_rank), ckey(admin_key), force_active = FALSE, protected = TRUE) - if(!CONFIG_GET(flag/admin_legacy_system) || dbfail) + if(!CONFIG_GET(flag/admin_legacy_system) && !dbfail) var/datum/db_query/query_load_admins = SSdbcore.NewQuery("SELECT ckey, `rank`, feedback FROM [format_table_name("admin")] ORDER BY `rank`") if(!query_load_admins.Execute()) message_admins("Error loading admins from database. Loading from backup.") @@ -275,6 +266,9 @@ GLOBAL_PROTECT(protected_ranks) var/datum/admins/admin_holder = new(admin_ranks, admin_ckey) admin_holder.cached_feedback_link = admin_feedback || NO_FEEDBACK_LINK qdel(query_load_admins) + if (!no_update) + save_admin_backup() + sync_admins_with_db() //load admins from backup file if(dbfail) if(!backup_file_json) @@ -286,14 +280,15 @@ GLOBAL_PROTECT(protected_ranks) log_world("Unable to locate admins backup file.") return backup_file_json = json_decode(backup_file) - for(var/J in backup_file_json["admins"]) + for(var/backup_admin_ckey in backup_file_json["admins"]) var/skip - for(var/A in GLOB.admin_datums + GLOB.deadmins) - if(A == "[J]") //this admin was already loaded from txt override + for(var/admin_ckey in GLOB.admin_datums + GLOB.deadmins) + if(ckey(admin_ckey) == ckey("[backup_admin_ckey]")) //this admin was already loaded from txt override skip = TRUE + break if(skip) continue - new /datum/admins(ranks_from_rank_name(backup_file_json["admins"]["[J]"]), ckey("[J]")) + new /datum/admins(ranks_from_rank_name(backup_file_json["admins"]["[backup_admin_ckey]"]), ckey("[backup_admin_ckey]")) #ifdef TESTING var/msg = "Admins Built:\n" for(var/ckey in GLOB.admin_datums) @@ -302,3 +297,103 @@ GLOBAL_PROTECT(protected_ranks) testing(msg) #endif return dbfail + + +/proc/sync_ranks_with_db() + set waitfor = FALSE + + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.", confidential = TRUE) + return + + var/list/sql_ranks = list() + for(var/datum/admin_rank/R as anything in GLOB.protected_ranks) + sql_ranks += list(list("rank" = R.name, "flags" = R.include_rights, "exclude_flags" = R.exclude_rights, "can_edit_flags" = R.can_edit_rights)) + SSdbcore.MassInsert(format_table_name("admin_ranks"), sql_ranks, duplicate_key = TRUE) + update_everything_flag_in_db() + + +/proc/update_everything_flag_in_db() + for(var/datum/admin_rank/R as anything in GLOB.admin_ranks) + var/list/flags = list() + if(R.include_rights == R_EVERYTHING) + flags += "flags" + if(R.exclude_rights == R_EVERYTHING) + flags += "exclude_flags" + if(R.can_edit_rights == R_EVERYTHING) + flags += "can_edit_flags" + if(!flags.len) + continue + var/flags_to_check = flags.Join(" != [R_EVERYTHING] AND ") + " != [R_EVERYTHING]" + var/datum/db_query/query_check_everything_ranks = SSdbcore.NewQuery( + "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = :rank AND ([flags_to_check])", + list("rank" = R.name) + ) + if(!query_check_everything_ranks.Execute()) + qdel(query_check_everything_ranks) + return + if(query_check_everything_ranks.NextRow()) //no row is returned if the rank already has the correct flag value + var/flags_to_update = flags.Join(" = [R_EVERYTHING], ") + " = [R_EVERYTHING]" + var/datum/db_query/query_update_everything_ranks = SSdbcore.NewQuery( + "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = :rank", + list("rank" = R.name) + ) + if(!query_update_everything_ranks.Execute()) + qdel(query_update_everything_ranks) + return + qdel(query_update_everything_ranks) + qdel(query_check_everything_ranks) + + +/proc/sync_admins_with_db() + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.") + return + + if(CONFIG_GET(flag/admin_legacy_system) || !SSdbcore.IsConnected()) //we're already using legacy system so there's nothing to save + return + sync_ranks_with_db() + var/list/sql_admins = list() + for(var/holder_ckey in GLOB.protected_admins) + var/datum/admins/holder = GLOB.protected_admins[holder_ckey] + sql_admins += list(list("ckey" = holder.target, "rank" = holder.rank_names())) + SSdbcore.MassInsert(format_table_name("admin"), sql_admins, duplicate_key = TRUE) + var/datum/db_query/query_admin_rank_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] AS p INNER JOIN [format_table_name("admin")] AS a ON p.ckey = a.ckey SET p.lastadminrank = a.rank") + query_admin_rank_update.Execute() + qdel(query_admin_rank_update) + + +/proc/save_admin_backup() + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Admin rank DB Sync blocked: Advanced ProcCall detected.") + return + + if(CONFIG_GET(flag/admin_legacy_system)) //we're already using legacy system so there's nothing to save + return + + //json format backup file generation stored per server + var/json_file = file("data/admins_backup.json") + var/list/file_data = list( + "ranks" = list(), + "admins" = list() + ) + for(var/datum/admin_rank/R as anything in GLOB.admin_ranks) + file_data["ranks"]["[R.name]"] = list() + file_data["ranks"]["[R.name]"]["include rights"] = R.include_rights + file_data["ranks"]["[R.name]"]["exclude rights"] = R.exclude_rights + file_data["ranks"]["[R.name]"]["can edit rights"] = R.can_edit_rights + + for(var/admin_ckey in GLOB.admin_datums + GLOB.deadmins) + var/datum/admins/admin = GLOB.admin_datums[admin_ckey] + + if(!admin) + admin = GLOB.deadmins[admin_ckey] + if (!admin) + continue + + file_data["admins"][admin_ckey] = admin.rank_names() + + admin.backup_connections() + + fdel(json_file) + WRITE_FILE(json_file, json_encode(file_data, JSON_PRETTY_PRINT)) diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index 9d2525ed8fa..df08496ec2a 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -232,7 +232,7 @@ GLOBAL_PROTECT(href_token) return VALID_2FA_CONNECTION if (!SSdbcore.Connect()) - if (verify_backup_data(client) || (client.ckey in GLOB.protected_admins)) + if (verify_admin_from_local_cache(client) || (client.ckey in GLOB.protected_admins)) return VALID_2FA_CONNECTION else return list(FALSE, null) @@ -249,7 +249,8 @@ GLOBAL_PROTECT(href_token) )) if (!query.Execute()) - qdel(query) + if (verify_admin_from_local_cache(client) || (client.ckey in GLOB.protected_admins)) + return VALID_2FA_CONNECTION return list(FALSE, null) var/is_valid = FALSE @@ -319,25 +320,30 @@ GLOBAL_PROTECT(href_token) #undef ERROR_2FA_REQUEST_PERMISSIONS -/datum/admins/proc/verify_backup_data(client/client) - var/backup_file = file2text("data/admins_backup.json") +/// Returns true if the admin's cid/ip is verified in the local cache +/datum/admins/proc/verify_admin_from_local_cache(client/client) + var/backup_filename = "data/admin_connections/[ckey(client?.ckey)].json" + if (!fexists(backup_filename)) + return FALSE + var/backup_file = file2text(backup_filename) if (isnull(backup_file)) - log_world("Unable to locate admins backup file.") + log_world("Unable to load admin connection's last_connections.json backup file.") return FALSE - var/list/backup_file_json = json_decode(backup_file) - var/connections = backup_file_json["connections"] + var/list/connections = json_decode(backup_file) - // This can happen for older admins_backup.json files if (isnull(connections)) return FALSE - var/most_recent_valid_connection = connections[client?.ckey] - if (isnull(most_recent_valid_connection)) - return FALSE + for (var/list/connection as anything in connections) + if (!islist(connection) || length(connection) < 2) + stack_trace("Invalid connection in admin connections backup file for `[client]`.") + continue + if (connection["cid"] == client?.computer_id && connection["ip"] == client?.address) + return TRUE + + return FALSE - return most_recent_valid_connection["cid"] == client?.computer_id \ - && most_recent_valid_connection["ip"] == client?.address /datum/admins/proc/alert_2fa_necessary(client/client) var/msg = " is trying to join, but needs to verify their ckey." @@ -358,6 +364,46 @@ GLOBAL_PROTECT(href_token) confidential = TRUE, ) +/datum/admins/proc/backup_connections() + set waitfor = FALSE + if (!length(CONFIG_GET(string/admin_2fa_url))) + return + var/ckey = ckey(target) + if (!ckey) + CRASH("can't backup an admin datum assigned to a blank ckey") + + if (!SSdbcore.Connect()) + return + + var/datum/db_query/query = SSdbcore.NewQuery({" + SELECT cid, INET_NTOA(ip) as ip FROM [format_table_name("admin_connections")] + WHERE + ckey = :ckey AND verification_time IS NOT NULL + "}, list( + "ckey" = ckey, + )) + + if (!query.Execute()) + qdel(query) + return + var/list/admin_connections = list() + while (query.NextRow()) + admin_connections += LIST_VALUE_WRAP_LISTS(list( + "cid" = query.item[1], + "ip" = query.item[2], + )) + + qdel(query) + + if (length(admin_connections) < 1) + return + + + var/backup_file = "data/admin_connections/[ckey].json" + if (fexists(backup_file)) + fdel(backup_file) + WRITE_FILE(file(backup_file), json_encode(admin_connections, JSON_PRETTY_PRINT)) + /// Get the rank name of the admin /datum/admins/proc/rank_names() return join_admin_ranks(ranks)