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)