From 2b98749d329ae6b81f4d7fc981d898d6393e5ea5 Mon Sep 17 00:00:00 2001 From: Charlie Nolan Date: Sun, 10 Aug 2025 04:05:19 -0700 Subject: [PATCH] Make DB admin ranks more useful (#29906) * Make DB admin ranks more useful * Avoid error message when cancelling permission toggle, allow adding localhost admins as real admins. * Lint. * Don't SQL error when a previously-unseen player connects. * Use ckey of permission editor, not mob name. * Strikethrough * Order in the list. * Deadmin, readmin, and 2fa. * Correct merge error --------- Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> --- SQL/paradise_schema.sql | 25 +- SQL/updates/69-70.sql | 39 ++ code/__DEFINES/admin_defines.dm | 2 +- code/__DEFINES/misc_defines.dm | 2 +- code/__HELPERS/type2type.dm | 41 +- code/_globalvars/lists/misc_lists.dm | 25 + .../sections/admin_configuration.dm | 2 +- .../subsystem/tickets/SStickets.dm | 2 +- .../game/machinery/computer/communications.dm | 6 +- code/modules/admin/admin_ranks.dm | 155 ++++- code/modules/admin/admin_verbs.dm | 109 --- code/modules/admin/holder2.dm | 246 ++++--- code/modules/admin/misc_admin_procs.dm | 2 +- .../admin/permissionverbs/permissionedit.dm | 648 ++++++++++++++---- code/modules/admin/topic.dm | 113 ++- code/modules/client/2fa.dm | 14 +- code/modules/client/client_procs.dm | 66 +- code/modules/tgui/states/admin_state.dm | 2 +- config/example/config.toml | 2 +- html/panels.css | 5 +- 20 files changed, 1037 insertions(+), 469 deletions(-) create mode 100644 SQL/updates/69-70.sql diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql index da78d6a3ca8..3c16d6ae650 100644 --- a/SQL/paradise_schema.sql +++ b/SQL/paradise_schema.sql @@ -171,14 +171,32 @@ DROP TABLE IF EXISTS `admin`; CREATE TABLE `admin` ( `id` int(11) NOT NULL AUTO_INCREMENT, `ckey` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, - `admin_rank` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Administrator', - `level` int(2) NOT NULL DEFAULT '0', - `flags` int(16) NOT NULL DEFAULT '0', + `display_rank` varchar(32) COLLATE utf8mb4_unicode_ci, + `permissions_rank` int(11) COMMENT 'Foreign key for admin_ranks.id', + `extra_permissions` int(16) NOT NULL DEFAULT '0', + `removed_permissions` int(16) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `ckey` (`ckey`) ) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `admin_ranks` +-- + +DROP TABLE IF EXISTS `admin_ranks`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `admin_ranks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_permissions` int(16) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `name` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `admin_log` -- @@ -273,7 +291,6 @@ CREATE TABLE `player` ( `lastseen` datetime NOT NULL, `ip` varchar(18) COLLATE utf8mb4_unicode_ci NOT NULL, `computerid` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, - `lastadminrank` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Player', `ooccolor` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT '#b82e00', `UI_style` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT 'Midnight', `UI_style_color` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT '#ffffff', diff --git a/SQL/updates/69-70.sql b/SQL/updates/69-70.sql new file mode 100644 index 00000000000..38fb9b5a344 --- /dev/null +++ b/SQL/updates/69-70.sql @@ -0,0 +1,39 @@ +# Updating DB from 69-70 -FunnyMan3595 +# Adds the admin_ranks table + +# Create the new table. +CREATE TABLE `admin_ranks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_permissions` int(16) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `name` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8mb4; + +# Create official ranks for every existing display rank, with the minimum common permissions. +INSERT INTO admin_ranks (name, default_permissions) +SELECT admin_rank, BIT_AND(flags) FROM admin GROUP BY admin_rank; + +# Add new columns to admin table. +ALTER TABLE admin + ADD COLUMN `display_rank` varchar(32) COLLATE utf8mb4_unicode_ci, + ADD COLUMN `permissions_rank` int(11) COMMENT 'Foreign key for admin_ranks.id', + ADD COLUMN `extra_permissions` int(16) NOT NULL DEFAULT '0', + ADD COLUMN `removed_permissions` int(16) NOT NULL DEFAULT '0'; + +# Set the rank and any extra_permissions for each admin. +# There won't be any removed_permissions because we took the minimum common permissions earlier. +# We also leave display_rank blank, since admin_ranks.name matches their old admin_rank. +UPDATE admin JOIN admin_ranks ON admin.admin_rank = admin_ranks.name +SET + admin.permissions_rank = admin_ranks.id, + admin.extra_permissions = admin.flags & ~admin_ranks.default_permissions; + +# Drop the old columns from the admin table. +ALTER TABLE admin + DROP COLUMN admin_rank, + DROP COLUMN level, + DROP COLUMN flags; + +# Drop the lastadminrank column from the player table. +ALTER TABLE player DROP COLUMN lastadminrank; diff --git a/code/__DEFINES/admin_defines.dm b/code/__DEFINES/admin_defines.dm index a9539d20135..e9e80b91110 100644 --- a/code/__DEFINES/admin_defines.dm +++ b/code/__DEFINES/admin_defines.dm @@ -41,7 +41,7 @@ #define R_MAINTAINER (1<<17) #define R_DEV_TEAM (1<<18) #define R_VIEWLOGS (1<<19) -// Update the following two defines if you add more +// Update the following two defines and GLOB.admin_permission_names if you add more #define R_MAXPERMISSION (1<<19) // This holds the maximum value for a permission. It is used in iteration, so keep it updated. diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm index a46d6ccd65a..5db8793c000 100644 --- a/code/__DEFINES/misc_defines.dm +++ b/code/__DEFINES/misc_defines.dm @@ -438,7 +438,7 @@ #define INVESTIGATE_DEATHS "deaths" // The SQL version required by this version of the code -#define SQL_VERSION 69 +#define SQL_VERSION 70 // Vending machine stuff #define CAT_NORMAL (1<<0) diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm index 243bc0e6205..4135db31786 100644 --- a/code/__HELPERS/type2type.dm +++ b/code/__HELPERS/type2type.dm @@ -34,7 +34,7 @@ num_list += text2num(x) return num_list -//Splits the text of a file at seperator and returns them in a list. +//Splits the text of a file at separator and returns them in a list. /proc/file2list(filename, separator = "\n", no_empty = TRUE) var/list/result = list() for(var/line in splittext(return_file_text(filename), separator)) @@ -132,28 +132,23 @@ if(BLEND_SUBTRACT) return ICON_SUBTRACT else return ICON_OVERLAY -//Converts a rights bitfield into a string -/proc/rights2text(rights,seperator="") - if(rights & R_BUILDMODE) . += "[seperator]+BUILDMODE" - if(rights & R_ADMIN) . += "[seperator]+ADMIN" - if(rights & R_BAN) . += "[seperator]+BAN" - if(rights & R_EVENT) . += "[seperator]+EVENT" - if(rights & R_SERVER) . += "[seperator]+SERVER" - if(rights & R_DEBUG) . += "[seperator]+DEBUG" - if(rights & R_POSSESS) . += "[seperator]+POSSESS" - if(rights & R_PERMISSIONS) . += "[seperator]+PERMISSIONS" - if(rights & R_STEALTH) . += "[seperator]+STEALTH" - if(rights & R_REJUVINATE) . += "[seperator]+REJUVINATE" - if(rights & R_VAREDIT) . += "[seperator]+VAREDIT" - if(rights & R_SOUNDS) . += "[seperator]+SOUND" - if(rights & R_SPAWN) . += "[seperator]+SPAWN" - if(rights & R_PROCCALL) . += "[seperator]+PROCCALL" - if(rights & R_MOD) . += "[seperator]+MODERATOR" - if(rights & R_MENTOR) . += "[seperator]+MENTOR" - if(rights & R_VIEWRUNTIMES) . += "[seperator]+VIEWRUNTIMES" - if(rights & R_MAINTAINER) . += "[seperator]+MAINTAINER" - if(rights & R_DEV_TEAM) . += "[seperator]+DEV_TEAM" - if(rights & R_VIEWLOGS) . += "[seperator]+VIEWLOGS" +///Converts a rights bitfield into a string +/proc/rights2text(rights,separator="") + . = "" + for(var/bit in GLOB.admin_permission_names) + if(rights & bit) + . += "[separator]+[GLOB.admin_permission_names[bit]]" + +///Converts the full permissions details of a DB-ranked admin to a HTML-colored string +/proc/ranked_rights2text(rank_rights, extra_rights, removed_rights, separator="") + . = "" + for(var/bit in GLOB.admin_permission_names) + if(removed_rights & bit) + . += "[separator]-[GLOB.admin_permission_names[bit]]" + else if(extra_rights & bit) + . += "[separator]+[GLOB.admin_permission_names[bit]]" + else if(rank_rights & bit) + . += "[separator]+[GLOB.admin_permission_names[bit]]" /proc/ui_style2icon(ui_style) switch(ui_style) diff --git a/code/_globalvars/lists/misc_lists.dm b/code/_globalvars/lists/misc_lists.dm index 7ea33c03f53..208dfd7043c 100644 --- a/code/_globalvars/lists/misc_lists.dm +++ b/code/_globalvars/lists/misc_lists.dm @@ -73,3 +73,28 @@ GLOBAL_LIST_EMPTY(rnd_tech_id_to_name) /// List of things that restrain teslas. GLOBAL_LIST_EMPTY(tesla_containment) + +/// Assoc list of admin permission names +GLOBAL_LIST_INIT(admin_permission_names, alist( + R_BUILDMODE = "BUILDMODE", + R_ADMIN = "ADMIN", + R_BAN = "BAN", + R_EVENT = "EVENT", + R_SERVER = "SERVER", + R_DEBUG = "DEBUG", + R_POSSESS = "POSSESS", + R_PERMISSIONS = "PERMISSIONS", + R_STEALTH = "STEALTH", + R_REJUVINATE = "REJUVINATE", + R_VAREDIT = "VAREDIT", + R_SOUNDS = "SOUNDS", + R_SPAWN = "SPAWN", + R_MOD = "MOD", + R_MENTOR = "MENTOR", + R_PROCCALL = "PROCCALL", + R_VIEWRUNTIMES = "VIEWRUNTIMES", + R_MAINTAINER = "MAINTAINER", + R_DEV_TEAM = "DEV_TEAM", + R_VIEWLOGS = "VIEWLOGS", +)) +GLOBAL_PROTECT(admin_permission_names) diff --git a/code/controllers/configuration/sections/admin_configuration.dm b/code/controllers/configuration/sections/admin_configuration.dm index 9050bd79a5d..5a2f9bdccee 100644 --- a/code/controllers/configuration/sections/admin_configuration.dm +++ b/code/controllers/configuration/sections/admin_configuration.dm @@ -32,7 +32,7 @@ if(islist(data["admin_assignments"])) ckey_rank_map.Cut() for(var/list/kvset in data["admin_assignments"]) - ckey_rank_map[kvset["ckey"]] = kvset["rank"] + ckey_rank_map[ckey(kvset["ckey"])] = kvset["rank"] // Load admin colours if(islist(data["admin_rank_colour_map"])) diff --git a/code/controllers/subsystem/tickets/SStickets.dm b/code/controllers/subsystem/tickets/SStickets.dm index 2b90e822867..0471e684587 100644 --- a/code/controllers/subsystem/tickets/SStickets.dm +++ b/code/controllers/subsystem/tickets/SStickets.dm @@ -585,7 +585,7 @@ UI STUFF for(var/client/X in GLOB.admins) if(ckey(X.ckey) == ckey(T.client_ckey)) continue - if(!check_rights_for(X, rights_needed)) + if(!check_rights_client(rights_needed, FALSE, X)) continue for(var/key in X.pm_tracker.pms) if(ckey(key) != ckey(T.client_ckey)) diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index 83ce0497dcc..681aa24b619 100644 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -12,7 +12,7 @@ #define COMM_MSGLEN_MINIMUM 6 #define COMM_CCMSGLEN_MINIMUM 20 -#define ADMIN_CHECK(user) (check_rights_all(R_ADMIN|R_EVENT, FALSE, user) && (authenticated >= COMM_AUTHENTICATION_CENTCOM || user.can_admin_interact())) +#define ADMIN_CHECK(user) (check_rights(R_ADMIN|R_EVENT, FALSE, user, all = TRUE) && (authenticated >= COMM_AUTHENTICATION_CENTCOM || user.can_admin_interact())) // The communications computer /obj/machinery/computer/communications @@ -56,7 +56,7 @@ . = ..() /obj/machinery/computer/communications/proc/is_authenticated(mob/user, message = 1) - if(check_rights_all(R_ADMIN|R_EVENT, FALSE, user)) + if(check_rights(R_ADMIN|R_EVENT, FALSE, user, all = TRUE)) if(user.can_admin_interact()) return COMM_AUTHENTICATION_AGHOST if(authenticated == COMM_AUTHENTICATION_CENTCOM) @@ -109,7 +109,7 @@ if(ACCESS_CAPTAIN in access) authenticated = COMM_AUTHENTICATION_CAPT if(ACCESS_CENT_COMMANDER in access) - if(!check_rights_all(R_ADMIN|R_EVENT, FALSE, ui.user)) + if(!check_rights(R_ADMIN|R_EVENT, FALSE, ui.user, all = TRUE)) to_chat(ui.user, "[src] buzzes, invalid central command clearance.") return authenticated = COMM_AUTHENTICATION_CENTCOM diff --git a/code/modules/admin/admin_ranks.dm b/code/modules/admin/admin_ranks.dm index 596dc3251eb..8ce0d74bda3 100644 --- a/code/modules/admin/admin_ranks.dm +++ b/code/modules/admin/admin_ranks.dm @@ -48,41 +48,119 @@ GLOBAL_PROTECT(admin_ranks) // this shit is being protected for obvious reasons testing(msg) #endif +/proc/reload_one_admin(admin_ckey, silent = FALSE) + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Admin reload blocked: Advanced ProcCall detected.") + message_admins("[key_name(usr)] attempted to reload an admin via advanced proc-call") + log_admin("[key_name(usr)] attempted to reload an admin via advanced proc-call") + return + + // Make sure it's actually in ckey format. + admin_ckey = ckey(admin_ckey) + + // Remove any existing permissions. + var/datum/admins/admin_datum = GLOB.admin_datums[admin_ckey] + if(admin_datum) + qdel(admin_datum) + + if(admin_ckey in (GLOB.de_admins + GLOB.de_mentors)) + if(!silent) + message_admins("Admin permissions for [admin_ckey] will reload when they re-admin.") + return + + if(admin_ckey in GLOB.directory) + var/client/admin = GLOB.directory[admin_ckey] + var/localhosted = admin.try_localhost_autoadmin() + if(localhosted) + if(!silent) + message_admins("Admin permissions for [admin_ckey] reloaded. Full permissions granted as they are currently connected from localhost.") + return + + if(GLOB.configuration.admin.use_database_admins) + var/datum/db_query/get_admin = SSdbcore.NewQuery({" + SELECT + -- Use the display_rank if set, otherwise the name of their permissions_rank. + IFNULL(admin.display_rank, admin_ranks.name), + -- Permissions start with their admin rank permissions (if any) + (IFNULL(admin_ranks.default_permissions, 0) + -- Then add in any extra permissions they've been granted. + | admin.extra_permissions) + -- And exclude any permissions they've had removed. + & ~admin.removed_permissions + FROM admin + -- We want all admins, and admin_ranks where available. + LEFT OUTER JOIN admin_ranks + ON admin.permissions_rank = admin_ranks.id + WHERE admin.ckey=:admin_ckey"}, list( + "admin_ckey" = admin_ckey + )) + if(!get_admin.warn_execute()) + qdel(get_admin) + return + + if(get_admin.NextRow()) + var/rank = get_admin.item[1] + var/rights = get_admin.item[2] + if(rights == 0) + // If you have no rights, you don't get an admin datum. + qdel(get_admin) + if(!silent) + message_admins("Admin permissions for [admin_ckey] have been reloaded.") + return + admin_datum = new(rank, rights, admin_ckey) + qdel(get_admin) + else + var/rank = GLOB.configuration.admin.ckey_rank_map[admin_ckey] + + // Load permissions associated with this rank + var/rights = GLOB.admin_ranks[rank] + + // Create their admin datum. + admin_datum = new /datum/admins(rank, rights, admin_ckey) + + // Set up their permissions. + if(admin_datum) + admin_datum.associate(GLOB.directory[admin_ckey]) + + if(admin_ckey in GLOB.directory) + var/client/admin = GLOB.directory[admin_ckey] + to_chat(admin, "Holder? [admin.holder] Permissions? [admin.holder?.rights]") + if(!silent) + message_admins("Admin permissions for [admin_ckey] have been reloaded.") + /proc/load_admins(run_async = FALSE) if(IsAdminAdvancedProcCall()) to_chat(usr, "Admin reload blocked: Advanced ProcCall detected.") message_admins("[key_name(usr)] attempted to reload admins via advanced proc-call") log_admin("[key_name(usr)] attempted to reload admins via advanced proc-call") return - //clear the datums references + + // Revoke all permissions. + var/list/localhost_admins = list() + for(var/datum/admins/admin_datum in GLOB.admin_datums) + if(admin_datum.is_localhost_autoadmin && admin_datum.owner) + localhost_admins += admin_datum.owner + qdel(admin_datum) GLOB.admin_datums.Cut() - for(var/client/C in GLOB.admins) - C.hide_verbs() - C.holder = null GLOB.admins.Cut() - // Remove all profiler access + // Just to be double-sure, revoke all profiler access for(var/A in world.GetConfig("admin")) world.SetConfig("APP/admin", A, null) if(!GLOB.configuration.admin.use_database_admins) load_admin_ranks() - //process each line seperately - for(var/iterator_key in GLOB.configuration.admin.ckey_rank_map) - var/ckey = ckey(iterator_key) // Snip out formatting - var/rank = GLOB.configuration.admin.ckey_rank_map[iterator_key] + for(var/ckey in GLOB.configuration.admin.ckey_rank_map) + var/rank = GLOB.configuration.admin.ckey_rank_map[ckey] - //load permissions associated with this rank + // Load permissions associated with this rank var/rights = GLOB.admin_ranks[rank] - //create the admin datum and store it for later use + // Create their admin datum. var/datum/admins/D = new /datum/admins(rank, rights, ckey) - if(D.rights & R_DEBUG || D.rights & R_VIEWRUNTIMES) // Grants profiler access to anyone with R_DEBUG or R_VIEWRUNTIMES - world.SetConfig("APP/admin", ckey, "role=admin") - - //find the client for a ckey if they are connected and associate them with the new admin datum + // Set up their admin permissions. D.associate(GLOB.directory[ckey]) else @@ -93,34 +171,47 @@ GLOBAL_PROTECT(admin_ranks) // this shit is being protected for obvious reasons load_admins() return - var/datum/db_query/query = SSdbcore.NewQuery("SELECT ckey, admin_rank, level, flags FROM admin") - if(!query.warn_execute(async=run_async)) - qdel(query) + var/datum/db_query/get_admins = SSdbcore.NewQuery({" + SELECT + admin.ckey, + -- Use the display_rank if set, otherwise the name of their permissions_rank. + IFNULL(admin.display_rank, admin_ranks.name), + -- Permissions start with their admin rank permissions (if any) + (IFNULL(admin_ranks.default_permissions, 0) + -- Then add in any extra permissions they've been granted. + | admin.extra_permissions) + -- And exclude any permissions they've had removed. + & ~admin.removed_permissions + FROM admin + -- We want all admins, and admin_ranks where available. + LEFT OUTER JOIN admin_ranks + ON admin.permissions_rank = admin_ranks.id"}) + if(!get_admins.warn_execute(async=run_async)) + qdel(get_admins) return - while(query.NextRow()) - var/ckey = query.item[1] - var/rank = query.item[2] - if(rank == "Removed") continue //This person was de-adminned. They are only in the admin list for archive purposes. - - var/rights = query.item[4] - if(istext(rights)) rights = text2num(rights) + while(get_admins.NextRow()) + var/ckey = get_admins.item[1] + var/rank = get_admins.item[2] + var/rights = get_admins.item[3] + if(rights == 0) + // If you have no rights, you don't get an admin datum. + continue var/datum/admins/D = new /datum/admins(rank, rights, ckey) - if(D.rights & R_DEBUG || D.rights & R_VIEWRUNTIMES) // Grants profiler access to anyone with R_DEBUG or R_VIEWRUNTIMES - world.SetConfig("APP/admin", ckey, "role=admin") - - //find the client for a ckey if they are connected and associate them with the new admin datum + // Set up their admin permissions. D.associate(GLOB.directory[ckey]) + qdel(get_admins) - qdel(query) - - if(!GLOB.admin_datums) + if(!length(GLOB.admin_datums)) log_world("The database query in load_admins() resulted in no admins being added to the list. Reverting to legacy system.") GLOB.configuration.admin.use_database_admins = FALSE load_admins() return + for(var/client/localhost_admin in localhost_admins) + localhost_admin.try_localhost_autoadmin() + #ifdef TESTING var/msg = "Admins Built:\n" for(var/ckey in GLOB.admin_datums) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index e86f94333a6..01cf86e37b6 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -879,119 +879,10 @@ GLOBAL_LIST_INIT(view_logs_verbs, list( log_admin("[key_name(usr)] deadmined themself.") message_admins("[key_name_admin(usr)] deadmined themself.") - if(check_rights(R_ADMIN, FALSE)) - GLOB.de_admins += ckey - else - GLOB.de_mentors += ckey deadmin() - add_verb(src, /client/proc/readmin) - update_active_keybindings() to_chat(src, "You are now a normal player.") SSblackbox.record_feedback("tally", "admin_verb", 1, "De-admin") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/readmin() - set name = "Re-admin self" - set category = "Admin" - set desc = "Regain your admin powers." - - var/datum/admins/D = GLOB.admin_datums[ckey] - var/rank = null - if(!GLOB.configuration.admin.use_database_admins) - for(var/iterator_key in GLOB.configuration.admin.ckey_rank_map) - var/_ckey = ckey(iterator_key) // Snip out formatting - if(ckey != _ckey) - continue - rank = GLOB.configuration.admin.ckey_rank_map[iterator_key] - break - else - if(!SSdbcore.IsConnected()) - to_chat(src, "Warning, MYSQL database is not connected.") - return - - var/datum/db_query/rank_read = SSdbcore.NewQuery( - "SELECT admin_rank FROM admin WHERE ckey=:ckey", - list("ckey" = ckey) - ) - - if(!rank_read.warn_execute()) - qdel(rank_read) - return FALSE - - while(rank_read.NextRow()) - rank = ckeyEx(rank_read.item[1]) - - qdel(rank_read) - if(!D) - D = try_localhost_autoadmin() - if(!D) - if(!GLOB.configuration.admin.use_database_admins) - if(GLOB.admin_ranks[rank] == null) - error("Error while re-adminning [src], admin rank ([rank]) does not exist.") - to_chat(src, "Error while re-adminning, admin rank ([rank]) does not exist.") - return - - // Do a little check here - if(GLOB.configuration.system.is_production && (GLOB.admin_ranks[rank] & R_ADMIN) && prefs._2fa_status == _2FA_DISABLED) // If they are an admin and their 2FA is disabled - to_chat(src,"You do not have 2FA enabled. Admin verbs will be unavailable until you have enabled 2FA.") // Very fucking obvious - return - D = new(rank, GLOB.admin_ranks[rank], ckey) - else - if(!SSdbcore.IsConnected()) - to_chat(src, "Warning, MYSQL database is not connected.") - return - - var/datum/db_query/admin_read = SSdbcore.NewQuery( - "SELECT ckey, admin_rank, flags FROM admin WHERE ckey=:ckey", - list("ckey" = ckey) - ) - - if(!admin_read.warn_execute()) - qdel(admin_read) - return FALSE - - while(admin_read.NextRow()) - var/admin_ckey = admin_read.item[1] - var/admin_rank = admin_read.item[2] - var/flags = admin_read.item[3] - if(!admin_ckey) - to_chat(src, "Error while re-adminning, ckey [admin_ckey] was not found in the admin database.") - qdel(admin_read) - return - if(admin_rank == "Removed") //This person was de-adminned. They are only in the admin list for archive purposes. - to_chat(src, "Error while re-adminning, ckey [admin_ckey] is not an admin.") - qdel(admin_read) - return - - if(istext(flags)) - flags = text2num(flags) - var/client/check_client = GLOB.directory[ckey] - // Do a little check here - if(GLOB.configuration.system.is_production && (flags & R_ADMIN) && check_client.prefs._2fa_status == _2FA_DISABLED) // If they are an admin and their 2FA is disabled - to_chat(src,"You do not have 2FA enabled. Admin verbs will be unavailable until you have enabled 2FA.") // Very fucking obvious - qdel(admin_read) - return - D = new(admin_rank, flags, ckey) - qdel(admin_read) - - var/client/C = GLOB.directory[ckey] - D.associate(C) - update_active_keybindings() - message_admins("[key_name_admin(usr)] re-adminned themselves.") - log_admin("[key_name(usr)] re-adminned themselves.") - GLOB.de_admins -= ckey - GLOB.de_mentors -= ckey - if(istype(mob, /mob/dead/observer)) - var/mob/dead/observer/O = mob - O.update_admin_actions() - SSblackbox.record_feedback("tally", "admin_verb", 1, "Re-admin") - return - else - to_chat(src, "You are already an admin.") - remove_verb(src, /client/proc/readmin) - GLOB.de_admins -= ckey - GLOB.de_mentors -= ckey - return - /client/proc/toggle_log_hrefs() set name = "Toggle href logging" set category = "Server" diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index 4ecb375ecec..9f1ee097481 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -10,10 +10,15 @@ GLOBAL_PROTECT(href_token) . += "[rand(10)]" /datum/admins - var/rank = "Temporary Admin" + var/rank = "No Rank" + var/ckey var/client/owner /// Bitflag containing the current rights this admin holder is assigned to var/rights = 0 + /// Tracks if their permissions have been reduced due to a lack of 2fa. + var/restricted_by_2fa = FALSE + /// Was this auto-created for someone connecting from localhost? + var/is_localhost_autoadmin = FALSE var/fakekey var/big_brother = FALSE var/current_tab = 0 @@ -27,7 +32,7 @@ GLOBAL_PROTECT(href_token) /// Our index into GLOB.antagonist_teams, so that admins can have pretty tabs in the Check Teams menu. var/team_switch_tab_index = 1 -/datum/admins/New(initial_rank = "Temporary Admin", initial_rights = 0, ckey) +/datum/admins/New(initial_rank, initial_rights, ckey) if(IsAdminAdvancedProcCall()) to_chat(usr, "Admin rank creation blocked: Advanced ProcCall detected.") message_admins("[key_name(usr)] attempted to create a new admin rank via advanced proc-call") @@ -37,8 +42,11 @@ GLOBAL_PROTECT(href_token) error("Admin datum created without a ckey argument. Datum has been deleted") qdel(src) return - rank = initial_rank - rights = initial_rights + src.ckey = ckey + if(initial_rank) + rank = initial_rank + if(initial_rights) + rights = initial_rights href_token = GenerateToken() GLOB.admin_datums[ckey] = src @@ -48,22 +56,68 @@ GLOBAL_PROTECT(href_token) message_admins("[key_name(usr)] attempted to delete an admin rank via advanced proc-call") log_admin("[key_name(usr)] attempted to delete an admin rank via advanced proc-call") return + disassociate() ..() return QDEL_HINT_HARDDEL_NOW -/datum/admins/proc/associate(client/C) +/datum/admins/proc/associate(client/C, delay_2fa_complaint = FALSE) if(IsAdminAdvancedProcCall()) to_chat(usr, "Rank association blocked: Advanced ProcCall detected.") message_admins("[key_name(usr)] attempted to associate an admin rank to a new client via advanced proc-call") log_admin("[key_name(usr)] attempted to associate an admin rank to a new client via advanced proc-call") return - if(istype(C)) - owner = C - owner.holder = src - owner.add_admin_verbs() //TODO - remove_verb(owner, /client/proc/readmin) - owner.init_verbs() //re-initialize the verb list - GLOB.admins |= C + + if(istype(C) && C.ckey != ckey) + CRASH("Admin datum belonging to [ckey] was asked to associate to client belonging to [C.ckey].") + + owner = C + + // Check for 2fa, if required. + if(needs_2fa() && istype(owner) && !owner.has_2fa()) + // Disable most permissions. + rights = rights & (R_MENTOR | R_VIEWRUNTIMES) + restricted_by_2fa = TRUE + if(!delay_2fa_complaint) + // And tell them about it. + to_chat(owner,"You do not have 2FA enabled. Admin verbs will be unavailable until you have enabled 2FA.") // Very fucking obvious + + // Regardless of client, tell BYOND if they should have profiler access. + if(rights & (R_DEBUG | R_VIEWRUNTIMES)) + world.SetConfig("APP/admin", ckey, "role=admin") + + if(!istype(owner)) + return + + owner.holder = src + owner.add_admin_verbs() + if(istype(owner.mob, /mob/dead/observer)) + var/mob/dead/observer/ghost = owner.mob + ghost.update_admin_actions() + remove_verb(owner, /client/proc/readmin) + owner.init_verbs() //re-initialize the verb list + owner.update_active_keybindings() + if(rights & (R_DEBUG | R_VIEWRUNTIMES)) + // Enable the buttons they should have. + winset(owner, "debugmcbutton", "is-disabled=true") + winset(owner, "profilecode", "is-disabled=true") + GLOB.admins |= owner + GLOB.de_admins -= ckey + GLOB.de_mentors -= ckey + +/datum/admins/proc/needs_2fa() + if((rights & (R_MENTOR | R_VIEWRUNTIMES)) == rights) + // No significant permissions. + return FALSE + + if(GLOB.configuration.admin.enable_localhost_autoadmin && owner && owner.is_connecting_from_localhost()) + // Localhost connections are immune. + return FALSE + + if(!GLOB.configuration.system.is_production) + // 2fa isn't required for anyone. + return FALSE + + return TRUE /datum/admins/proc/disassociate() if(IsAdminAdvancedProcCall()) @@ -71,72 +125,74 @@ GLOBAL_PROTECT(href_token) message_admins("[key_name(usr)] attempted to disassociate an admin rank from a client via advanced proc-call") log_admin("[key_name(usr)] attempted to disassociate an admin rank from a client via advanced proc-call") return + + GLOB.admin_datums -= ckey + + // Regardless of client, tell BYOND they shouldn't have access to the profiler. + world.SetConfig("APP/admin", ckey, null) + if(owner) GLOB.admins -= owner owner.hide_verbs() - owner.init_verbs() owner.holder = null + owner.init_verbs() + if(istype(owner.mob, /mob/dead/observer)) + var/mob/dead/observer/ghost = owner.mob + ghost.update_admin_actions() + owner.update_active_keybindings() + // Disable buttons they should no longer have. + winset(owner, "debugmcbutton", "is-disabled=true") + winset(owner, "profilecode", "is-disabled=true") owner = null /* -checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags) -if rights_required == 0, then it simply checks if they are an admin. -if it doesn't return 1 and show_msg=1 it will prints a message explaining why the check has failed -generally it would be used like so: +Checks if usr is an admin. +If rights_required == 0, then it simply checks if they are an admin. +If all is set, requires all the rights listed. Otherwise, any matching right is sufficient. +With show_msg, if the check fails, prints a message explaining why. -proc/admin_proc() - if(!check_rights(R_ADMIN)) return +Usage: +/some/proc() + if(!check_rights(R_ADMIN)) + return to_chat(world, "you have enough rights!") -NOTE: it checks usr! not src! So if you're checking somebody's rank in a proc which they did not call -you will have to do something like if(client.holder.rights & R_ADMIN) yourself. +NOTE: checks usr by default, not src */ -/proc/check_rights(rights_required, show_msg = TRUE, mob/user = usr) +/proc/check_rights(rights_required, show_msg = TRUE, mob/user = usr, all = FALSE) if(user && user.client) - if(rights_required) - if(user.client.holder) - if(rights_required & user.client.holder.rights) - return 1 - else - if(show_msg) - to_chat(user, "Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].") - else - if(user.client.holder) - return 1 - else - if(show_msg) - to_chat(user, "Error: You are not an admin.") - return 0 - -// Basically the above proc but checks at a /client level -/proc/check_rights_client(rights_required, show_msg = TRUE, client/C) - if(C) - if(rights_required) - if(C.holder) - if(rights_required & C.holder.rights) - return TRUE - else - if(show_msg) - to_chat(C, "Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].") - else - if(C.holder) - return TRUE - else - if(show_msg) - to_chat(C, "Error: You are not an admin.") + return check_rights_ckey(rights_required, show_msg, user.ckey, all) return FALSE -//probably a bit iffy - will hopefully figure out a better solution -/proc/check_if_greater_rights_than(client/other) - if(usr && usr.client) - if(usr.client.holder) - if(!other || !other.holder) - return 1 - if(usr.client.holder.rights != other.holder.rights) - if((usr.client.holder.rights & other.holder.rights) == other.holder.rights) - return 1 //we have all the rights they have and more - to_chat(usr, "Error: Cannot proceed. They have more or equal rights to us.") - return 0 +// As above, for a /client +/proc/check_rights_client(rights_required, show_msg, client/C, all = FALSE) + if(C) + return check_rights_ckey(rights_required, show_msg, C.ckey, all) + return FALSE + +// As above, for a ckey +/proc/check_rights_ckey(rights_required, show_msg, ckey, all) + var/datum/admins/holder = GLOB.admin_datums[ckey] + if(!holder) + if(show_msg) + to_chat(GLOB.directory[ckey], "Error: You are not an admin.") + return FALSE + + if(!rights_required) + return TRUE + + if((rights_required & holder.rights) == rights_required) + return TRUE + else if(!all && (rights_required & holder.rights)) + return TRUE + + if(show_msg) + if(all) + to_chat(GLOB.directory[ckey], "Error: You do not have sufficient rights to do that. You require all of the following flags:[rights2text(rights_required," ")].") + else + to_chat(GLOB.directory[ckey], "Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].") + + return FALSE /client/proc/deadmin() if(IsAdminAdvancedProcCall()) @@ -144,45 +200,39 @@ you will have to do something like if(client.holder.rights & R_ADMIN) yourself. message_admins("[key_name(usr)] attempted to de-admin a client via advanced proc-call") log_admin("[key_name(usr)] attempted to de-admin a client via advanced proc-call") return - GLOB.admin_datums -= ckey if(holder) - holder.disassociate() + if(check_rights(R_ADMIN, show_msg = FALSE)) + GLOB.de_admins += ckey + else + GLOB.de_mentors += ckey qdel(holder) - if(istype(mob, /mob/dead/observer)) - var/mob/dead/observer/O = mob - O.update_admin_actions() - return TRUE + add_verb(src, /client/proc/readmin) + else + to_chat(src, "Error: You were already not an admin.") -//This proc checks whether subject has at least ONE of the rights specified in rights_required. -/proc/check_rights_for(client/subject, rights_required) - if(subject && subject.holder) - if(rights_required && !(rights_required & subject.holder.rights)) - return 0 - return 1 - return 0 +/client/proc/readmin() + set name = "Re-admin self" + set category = "Admin" + set desc = "Regain your admin powers." -/** - * Requires the holder to have all the rights specified - * - * rights_required = R_ADMIN|R_EVENT means they must have both flags, or it will return false - */ -/proc/check_rights_all(rights_required, show_msg = TRUE, mob/user = usr) - if(!user?.client) - return FALSE - if(!rights_required) - if(user.client.holder) - return TRUE - if(show_msg) - to_chat(user, "Error: You are not an admin.") - return FALSE + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Readmin blocked: Advanced ProcCall detected.") + message_admins("[key_name(usr)] attempted to re-admin a client via advanced proc-call") + log_admin("[key_name(usr)] attempted to re-admin a client via advanced proc-call") - if(!user.client.holder) - return FALSE - if((user.client.holder.rights & rights_required) == rights_required) - return TRUE - if(show_msg) - to_chat(user, "Error: You do not have sufficient rights to do that. You require all of the following flags:[rights2text(rights_required, " ")].") - return FALSE + if(holder) + to_chat(usr, "You are already an admin.") + else if(ckey in (GLOB.de_admins + GLOB.de_mentors)) + GLOB.de_admins -= ckey + GLOB.de_mentors -= ckey + reload_one_admin(ckey, silent = TRUE) + log_admin("[key_name(usr)] re-adminned themselves.") + message_admins("[key_name_admin(usr)] re-adminned themselves.") + SSblackbox.record_feedback("tally", "admin_verb", 1, "Re-admin") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + else + to_chat(usr, "Readmin blocked: You were not an admin or mentor.") + message_admins("[key_name(usr)] attempted to re-admin without being an admin or mentor") + log_admin("[key_name(usr)] attempted to re-admin without being an admin or mentor") /datum/admins/vv_edit_var(var_name, var_value) return FALSE // no admin abuse diff --git a/code/modules/admin/misc_admin_procs.dm b/code/modules/admin/misc_admin_procs.dm index 46d03f32d04..f1251238bd5 100644 --- a/code/modules/admin/misc_admin_procs.dm +++ b/code/modules/admin/misc_admin_procs.dm @@ -1004,7 +1004,7 @@ GLOBAL_VAR_INIT(gamma_ship_location, 1) // 0 = station , 1 = space /proc/staff_countup(rank_mask = R_BAN) var/list/result = list(0, 0, 0) for(var/client/X in GLOB.admins) - if(rank_mask && !check_rights_for(X, rank_mask)) + if(rank_mask && !check_rights_client(rank_mask, FALSE, X)) result[2]++ continue if(X.holder.fakekey) diff --git a/code/modules/admin/permissionverbs/permissionedit.dm b/code/modules/admin/permissionverbs/permissionedit.dm index 4007266e95a..6be15acc9c5 100644 --- a/code/modules/admin/permissionverbs/permissionedit.dm +++ b/code/modules/admin/permissionverbs/permissionedit.dm @@ -13,6 +13,13 @@ var/datum/asset/permissions_asset = get_asset_datum(/datum/asset/simple/permissions) permissions_asset.send(usr) + var/db_section = "" + if(db_available()) + db_section = {" +
Admin database active.
All data is pulled from the database, and may require an admin reload to match actual permissions.
Automatically-granted localhost admin permissions are not listed.


+
Edit admin ranks
+ "} + var/output = {" @@ -21,6 +28,8 @@ +
+[db_section]
@@ -28,23 +37,75 @@ "} - for(var/adm_ckey in GLOB.admin_datums) - var/datum/admins/D = GLOB.admin_datums[adm_ckey] - if(!D) continue - var/rank = D.rank ? D.rank : "*none*" - var/rights = rights2text(D.rights," ") - if(!rights) rights = "*none*" - output += {" - - - -"} + var/list/admin_details = list() + if(GLOB.configuration.admin.use_database_admins) + var/datum/db_query/get_admins = SSdbcore.NewQuery({" + SELECT + admin.ckey, + admin_ranks.name, + admin.display_rank, + IFNULL(admin_ranks.default_permissions, 0), + admin.extra_permissions, + admin.removed_permissions + FROM admin + -- We want all admins, and admin_ranks where available. + LEFT OUTER JOIN admin_ranks + ON admin.permissions_rank = admin_ranks.id + ORDER BY admin_ranks.name ASC, admin.ckey ASC"}) + if(!get_admins.warn_execute()) + qdel(get_admins) + return - /*output += "" - output += "" - output += "" - output += "" - output += ""*/ + while(get_admins.NextRow()) + var/ckey = get_admins.item[1] + var/rank = get_admins.item[2] + var/display_rank = get_admins.item[3] + var/rank_permissions = get_admins.item[4] + var/extra_permissions = get_admins.item[5] + var/removed_permissions = get_admins.item[6] + + if(!rank && !display_rank && !extra_permissions && !removed_permissions) + // Former admin, no current rank or permissions. Don't bother showing them. + continue + + var/listed_rank + if(rank) + listed_rank = rank + else + listed_rank = "*none*" + if(display_rank) + listed_rank = "[display_rank] ([rank])" + var/display_rights = ranked_rights2text(rank_permissions, extra_permissions, removed_permissions," ") + if(display_rights == "") + display_rights = "*none*" + admin_details += list(list( + ckey, + listed_rank, + display_rights + )) + qdel(get_admins) + else + var/list/sorted_ckeys = list() + for(var/adm_ckey in GLOB.admin_datums) + sorted_ckeys += adm_ckey + sortTim(sorted_ckeys, cmp = GLOBAL_PROC_REF(cmp_text_asc)) + + for(var/adm_ckey in sorted_ckeys) + var/datum/admins/D = GLOB.admin_datums[adm_ckey] + if(!D) + continue + var/rank = D.rank ? D.rank : "*none*" + var/rights = rights2text(D.rights," ") + if(!rights) + rights = "*none*" + admin_details += list(list(adm_ckey, rank, rights)) + + for(var/list/details in admin_details) + output += {" + + + +"} output += {"
CKEY \[+\]
[adm_ckey] \[-\][rank][rights]
[adm_ckey] \[-\][rank][rights]
[details[1]] \[-\][details[2]][details[3]]
@@ -54,202 +115,525 @@ usr << browse(output,"window=editrights;size=600x500") -/datum/admins/proc/log_admin_rank_modification(adm_ckey, new_rank) - if(!GLOB.configuration.admin.use_database_admins) - return - - if(!usr.client) - return - - if(!check_rights(R_PERMISSIONS)) - return - +/datum/admins/proc/db_available() if(!SSdbcore.IsConnected()) - to_chat(usr, "Failed to establish database connection") + return FALSE + if(!GLOB.configuration.admin.use_database_admins) + return FALSE + return TRUE + +/datum/admins/proc/can_edit_db() + if(!check_rights(R_PERMISSIONS)) + return FALSE + if(IsAdminAdvancedProcCall()) + to_chat(usr, "Admin edit blocked: Advanced ProcCall detected.") + message_admins("[key_name(usr)] attempted to edit admin DB via advanced proc-call") + log_admin("[key_name(usr)] attempted to edit admin DB via advanced proc-call") + return FALSE + if(!usr.client) + return FALSE + if(!db_available()) + to_chat(usr, "Admin database unavailable") + return FALSE + return TRUE + +/datum/admins/proc/set_db_rank(target_key, new_rank, display_rank = null, clear_custom_permissions = FALSE) + if(!can_edit_db()) return - if(!adm_ckey || !new_rank) + if(!istext(target_key)) + to_chat(usr, "Provided key '[target_key]' is not text!") return - adm_ckey = ckey(adm_ckey) - - if(!adm_ckey) + target_key = ckey(target_key) + if(target_key == "") + to_chat(usr, "Provided key was blank after converting to ckey!") return - if(!istext(adm_ckey) || !istext(new_rank)) + if(!isnum(new_rank)) + to_chat(usr, "Provided rank ID '[new_rank]' is not a number!") return - var/datum/db_query/select_query = SSdbcore.NewQuery("SELECT id FROM admin WHERE ckey=:adm_ckey", list( - "adm_ckey" = adm_ckey + var/datum/db_query/get_rank_name = SSdbcore.NewQuery("SELECT name FROM admin_ranks WHERE id = :new_rank", list( + "new_rank" = new_rank )) - if(!select_query.warn_execute()) - qdel(select_query) + if(!get_rank_name.warn_execute()) + qdel(get_rank_name) return + var/rank_name + if(get_rank_name.NextRow()) + rank_name = get_rank_name.item[1] + qdel(get_rank_name) + if(!rank_name) + to_chat(usr, "Rank with ID [new_rank] not found in database!") + return + + var/datum/db_query/get_admin_id = SSdbcore.NewQuery("SELECT id FROM admin WHERE ckey = :target_key", list( + "target_key" = target_key + )) + if(!get_admin_id.warn_execute()) + qdel(get_admin_id) + return + + var/display_note = "" + if(display_rank) + display_note = " with the custom rank title [display_rank]" var/new_admin = TRUE var/admin_id - while(select_query.NextRow()) + if(get_admin_id.NextRow()) new_admin = FALSE - admin_id = text2num(select_query.item[1]) - qdel(select_query) - flag_account_for_forum_sync(adm_ckey) + admin_id = text2num(get_admin_id.item[1]) + qdel(get_admin_id) if(new_admin) - var/datum/db_query/insert_query = SSdbcore.NewQuery("INSERT INTO admin (`id`, `ckey`, `admin_rank`, `level`, `flags`) VALUES (null, :adm_ckey, :new_rank, -1, 0)", list( - "adm_ckey" = adm_ckey, - "new_rank" = new_rank + var/datum/db_query/insert_new_admin = SSdbcore.NewQuery("INSERT INTO admin (`ckey`, `permissions_rank`, `display_rank`) VALUES (:target_key, :new_rank, :display_rank)", list( + "target_key" = target_key, + "new_rank" = new_rank, + "display_rank" = display_rank )) - if(!insert_query.warn_execute()) - qdel(insert_query) + if(!insert_new_admin.warn_execute()) + qdel(insert_new_admin) return - qdel(insert_query) + qdel(insert_new_admin) + message_admins("Admin ranks updated by [usr.ckey]: [target_key] (NEW ADMIN) is now a [rank_name][display_note].") - var/logtxt = "Added new admin [adm_ckey] to rank [new_rank]" - var/datum/db_query/log_query = SSdbcore.NewQuery("INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (Now() , :uckey, :uip, :logtxt)", list( + var/logtxt = "Added new admin [target_key] to rank [rank_name][display_note]" + var/datum/db_query/add_log = SSdbcore.NewQuery("INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (Now() , :uckey, :uip, :logtxt)", list( "uckey" = usr.ckey, "uip" = usr.client.address, "logtxt" = logtxt )) - if(!log_query.warn_execute()) - qdel(log_query) + if(!add_log.warn_execute()) + qdel(add_log) return - qdel(log_query) - - to_chat(usr, "New admin added.") + qdel(add_log) else if(!isnull(admin_id) && isnum(admin_id)) - var/datum/db_query/insert_query = SSdbcore.NewQuery("UPDATE admin SET admin_rank=:new_rank WHERE id=:admin_id", list( + var/datum/db_query/update_admin_rank = SSdbcore.NewQuery("UPDATE admin SET permissions_rank = :new_rank, display_rank = :display_rank WHERE id = :admin_id", list( "new_rank" = new_rank, + "display_rank" = display_rank, "admin_id" = admin_id, )) - if(!insert_query.warn_execute()) - qdel(insert_query) + if(!update_admin_rank.warn_execute()) + qdel(update_admin_rank) return - qdel(insert_query) + qdel(update_admin_rank) + message_admins("Admin ranks updated by [usr.ckey]: [target_key] is now a [rank_name][display_note].") + var/logtxt = "Edited the rank of [target_key] to [rank_name][display_note]" - var/logtxt = "Edited the rank of [adm_ckey] to [new_rank]" - var/datum/db_query/log_query = SSdbcore.NewQuery("INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (Now() , :uckey, :uip, :logtxt)", list( + if(clear_custom_permissions) + var/datum/db_query/clear_permissions = SSdbcore.NewQuery("UPDATE admin SET extra_permissions = 0, removed_permissions = 0 WHERE id = :admin_id", list( + "admin_id" = admin_id, + )) + if(!clear_permissions.warn_execute()) + qdel(clear_permissions) + return + qdel(clear_permissions) + logtxt += " and cleared their custom permissions" + message_admins("Admin permissions updated by [usr.ckey]: [target_key] no longer has any custom permissions.") + + var/datum/db_query/add_log = SSdbcore.NewQuery("INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (Now() , :uckey, :uip, :logtxt)", list( "uckey" = usr.ckey, "uip" = usr.client.address, "logtxt" = logtxt, )) - if(!log_query.warn_execute()) - qdel(log_query) + if(!add_log.warn_execute()) + qdel(add_log) return - qdel(log_query) - to_chat(usr, "Admin rank changed.") + qdel(add_log) -/datum/admins/proc/log_admin_permission_modification(adm_ckey, new_permission) - if(IsAdminAdvancedProcCall()) - to_chat(usr, "Admin edit blocked: Advanced ProcCall detected.") - message_admins("[key_name(usr)] attempted to edit admin ranks via advanced proc-call") - log_admin("[key_name(usr)] attempted to edit admin ranks via advanced proc-call") - return - if(!GLOB.configuration.admin.use_database_admins) +/datum/admins/proc/toggle_db_permission(target_key, permission_bit) + if(!can_edit_db()) return - if(!usr.client) + if(!istext(target_key)) + to_chat(usr, "Provided key '[target_key]' is not text!") return - if(!check_rights(R_PERMISSIONS)) + target_key = ckey(target_key) + if(target_key == "") + to_chat(usr, "Provided key was blank after converting to ckey!") return - if(!SSdbcore.IsConnected()) - to_chat(usr, "Failed to establish database connection") + if(!isnum(permission_bit) || permission_bit < 1 || floor(permission_bit) != permission_bit) + to_chat(usr, "Provided permission '[permission_bit]' is not positive whole number!") return - if(!adm_ckey || !new_permission) + var/pow2 = round(log(2, permission_bit), 1) + if((2 ** pow2) != permission_bit) + to_chat(usr, "Provided permission '[permission_bit]' is not a power of two, and would affect multiple permission bits!") return - adm_ckey = ckey(adm_ckey) - - if(!adm_ckey) - return - - if(istext(new_permission)) - new_permission = text2num(new_permission) - - if(!istext(adm_ckey) || !isnum(new_permission)) - return - - var/datum/db_query/select_query = SSdbcore.NewQuery("SELECT id, flags FROM admin WHERE ckey=:adm_ckey", list( - "adm_ckey" = adm_ckey + var/datum/db_query/get_admin_permissions = SSdbcore.NewQuery("SELECT admin.id, admin_ranks.default_permissions, admin.extra_permissions, admin.removed_permissions FROM admin LEFT OUTER JOIN admin_ranks ON admin.permissions_rank = admin_ranks.id WHERE ckey = :target_key", list( + "target_key" = target_key )) - if(!select_query.warn_execute()) - qdel(select_query) + if(!get_admin_permissions.warn_execute()) + qdel(get_admin_permissions) return var/admin_id - var/admin_rights - while(select_query.NextRow()) - admin_id = text2num(select_query.item[1]) - admin_rights = text2num(select_query.item[2]) + var/rank_permissions = 0 + var/extra_permissions = 0 + var/removed_permissions = 0 + if(get_admin_permissions.NextRow()) + admin_id = get_admin_permissions.item[1] + // || 0 forces this to be numeric if they have no rank in the database. + rank_permissions = get_admin_permissions.item[2] || 0 + extra_permissions = get_admin_permissions.item[3] + removed_permissions = get_admin_permissions.item[4] - qdel(select_query) + qdel(get_admin_permissions) if(!admin_id) + to_chat(usr, "No admin found with ckey [target_key]!") return - flag_account_for_forum_sync(adm_ckey) - if(admin_rights & new_permission) //This admin already has this permission, so we are removing it. - var/datum/db_query/insert_query = SSdbcore.NewQuery("UPDATE admin SET flags=:newflags WHERE id=:admin_id", list( - "newflags" = (admin_rights & ~new_permission), + flag_account_for_forum_sync(target_key) + + if(removed_permissions & permission_bit) + // They had an exception removing this permission bit, let them have it. + var/datum/db_query/remove_removal = SSdbcore.NewQuery("UPDATE admin SET removed_permissions = removed_permissions & ~:permission_bit WHERE id = :admin_id", list( + "permission_bit" = permission_bit, "admin_id" = admin_id )) - if(!insert_query.warn_execute()) - qdel(insert_query) + if(!remove_removal.warn_execute()) + qdel(remove_removal) return - qdel(insert_query) + qdel(remove_removal) + message_admins("Admin permissions updated by [usr.ckey]: [target_key] is no longer excluded from having [rights2text(permission_bit)].") - var/logtxt = "Removed permission [rights2text(new_permission)] (flag = [new_permission]) to admin [adm_ckey]" - var/datum/db_query/log_query = SSdbcore.NewQuery({" + var/logtxt = "Un-excluded permission [rights2text(permission_bit)] (flag = [permission_bit]) from admin [target_key]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) VALUES (Now() , :uckey, :uip, :logtxt)"}, list( "uckey" = usr.ckey, "uip" = usr.client.address, "logtxt" = logtxt )) - if(!log_query.warn_execute()) - qdel(log_query) - return - qdel(log_query) - to_chat(usr, "Permission removed.") - else //This admin doesn't have this permission, so we are adding it. - var/datum/db_query/insert_query = SSdbcore.NewQuery("UPDATE admin SET flags=:newflags WHERE id=:admin_id", list( - "newflags" = (admin_rights | new_permission), + create_log.warn_execute() + qdel(create_log) + return + + if(extra_permissions & permission_bit) + // They had an exception adding this permission bit, remove it. + var/datum/db_query/remove_extra = SSdbcore.NewQuery("UPDATE admin SET extra_permissions = extra_permissions & ~:permission_bit WHERE id = :admin_id", list( + "permission_bit" = permission_bit, "admin_id" = admin_id )) - if(!insert_query.warn_execute()) - qdel(insert_query) + if(!remove_extra.warn_execute()) + qdel(remove_extra) return - qdel(insert_query) + qdel(remove_extra) + message_admins("Admin permissions updated by [usr.ckey]: [target_key] no longer has the extra permission [rights2text(permission_bit)].") - var/logtxt = "Added permission [rights2text(new_permission)] (flag = [new_permission]) to admin [adm_ckey]" - var/datum/db_query/log_query = SSdbcore.NewQuery({" + var/logtxt = "Removed extra permission [rights2text(permission_bit)] (flag = [permission_bit]) from admin [target_key]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) VALUES (Now() , :uckey, :uip, :logtxt)"}, list( "uckey" = usr.ckey, "uip" = usr.client.address, "logtxt" = logtxt )) + create_log.warn_execute() + qdel(create_log) + return - if(!log_query.warn_execute()) - qdel(log_query) + if(rank_permissions & permission_bit) + // They have this permission from their rank, and an exception removing it. + var/datum/db_query/create_removal = SSdbcore.NewQuery("UPDATE admin SET removed_permissions = removed_permissions | :permission_bit WHERE id = :admin_id", list( + "permission_bit" = permission_bit, + "admin_id" = admin_id + )) + if(!create_removal.warn_execute()) + qdel(create_removal) return - qdel(log_query) - to_chat(usr, "Permission added.") + qdel(create_removal) + message_admins("Admin permissions updated by [usr.ckey]: [target_key] is now excluded from having [rights2text(permission_bit)].") -/datum/admins/proc/updateranktodb(ckey,newrank) - if(!SSdbcore.IsConnected()) - return - if(!check_rights(R_PERMISSIONS)) + var/logtxt = "Excluded permission [rights2text(permission_bit)] (flag = [permission_bit]) from admin [target_key]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) return - var/datum/db_query/query_update = SSdbcore.NewQuery("UPDATE player SET lastadminrank=:admin_rank WHERE ckey=:ckey", list( - "admin_rank" = newrank, - "ckey" = ckey + // They did not have this permission from their rank, add an exception granting it. + var/datum/db_query/add_extra = SSdbcore.NewQuery("UPDATE admin SET extra_permissions = extra_permissions | :permission_bit WHERE id = :admin_id", list( + "permission_bit" = permission_bit, + "admin_id" = admin_id )) - if(!query_update.warn_execute()) - qdel(query_update) + if(!add_extra.warn_execute()) + qdel(add_extra) + return + qdel(add_extra) + message_admins("Admin permissions updated by [usr.ckey]: [target_key] has been granted the extra permission [rights2text(permission_bit)].") + + var/logtxt = "Added extra permission [rights2text(permission_bit)] (flag = [permission_bit]) to admin [target_key]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + +/datum/admins/proc/remove_db_admin(target_key, clear_custom_permissions = TRUE) + if(!can_edit_db()) return - qdel(query_update) - flag_account_for_forum_sync(ckey) + if(!istext(target_key)) + to_chat(usr, "Provided key '[target_key]' is not text!") + return + + target_key = ckey(target_key) + if(target_key == "") + to_chat(usr, "Provided key was blank after converting to ckey!") + return + + var/datum/db_query/get_admin_id = SSdbcore.NewQuery("SELECT admin.id FROM admin WHERE ckey = :target_key", list( + "target_key" = target_key + )) + if(!get_admin_id.warn_execute()) + qdel(get_admin_id) + return + + var/admin_id + if(get_admin_id.NextRow()) + admin_id = get_admin_id.item[1] + qdel(get_admin_id) + + if(!admin_id) + to_chat(usr, "No admin found with ckey [target_key]!") + return + + flag_account_for_forum_sync(target_key) + + var/datum/db_query/remove_admin = SSdbcore.NewQuery("UPDATE admin SET permissions_rank = NULL, display_rank = NULL WHERE id = :admin_id", list( + "admin_id" = admin_id, + )) + if(!remove_admin.warn_execute()) + qdel(remove_admin) + return + qdel(remove_admin) + message_admins("Admin ranks updated by [usr.ckey]: [target_key] no longer has any admin rank.") + var/logtxt = "Removed the admin rank of [target_key]" + + if(clear_custom_permissions) + var/datum/db_query/clear_permissions = SSdbcore.NewQuery("UPDATE admin SET extra_permissions = 0, removed_permissions = 0 WHERE id = :admin_id", list( + "admin_id" = admin_id, + )) + if(!clear_permissions.warn_execute()) + qdel(clear_permissions) + return + qdel(clear_permissions) + message_admins("Admin permissions updated by [usr.ckey]: [target_key] no longer has any custom permissions.") + logtxt += " and cleared their custom permissions" + + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + +/datum/admins/proc/create_db_rank(rank_name) + if(!can_edit_db()) + return + + if(get_db_rank_id(rank_name)) + // We already have it. + return + + var/datum/db_query/add_rank = SSdbcore.NewQuery("INSERT INTO admin_ranks SET name = :rank_name", list( + "rank_name" = rank_name + )) + add_rank.warn_execute() + qdel(add_rank) + + message_admins("Admin ranks updated by [usr.ckey]: new rank [rank_name] created.") + var/logtxt = "Created the admin rank [rank_name]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + return get_db_rank_id(rank_name) + +/datum/admins/proc/delete_db_rank(rank_name) + if(!can_edit_db()) + return + + var/rank_id = get_db_rank_id(rank_name) + if(!rank_id) + to_chat(usr, "No rank named [rank_name] found!") + return + + var/datum/db_query/get_admins_with_rank = SSdbcore.NewQuery("SELECT ckey FROM admin WHERE permissions_rank = :rank_id", list( + "rank_id" = rank_id + )) + if(!get_admins_with_rank.warn_execute()) + qdel(get_admins_with_rank) + return + var/list/admins = list() + while(get_admins_with_rank.NextRow()) + admins += get_admins_with_rank.item[1] + qdel(get_admins_with_rank) + if(length(admins) > 0) + to_chat(usr, "[rank_name] is still in use, reassign the following admins first: [admins.Join(", ")]") + return + + var/datum/db_query/delete_rank = SSdbcore.NewQuery("DELETE FROM admin_ranks WHERE id = :rank_id", list( + "rank_id" = rank_id + )) + if(!delete_rank.warn_execute()) + qdel(delete_rank) + return + qdel(delete_rank) + + message_admins("Admin ranks updated by [usr.ckey]: unused rank [rank_name] deleted.") + var/logtxt = "Deleted the unused admin rank [rank_name]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + +/datum/admins/proc/toggle_db_rank_permission(rank_name, permission_bit) + if(!can_edit_db()) + return + + var/datum/db_query/get_rank_details = SSdbcore.NewQuery("SELECT id, default_permissions FROM admin_ranks WHERE name = :rank_name", list( + "rank_name" = rank_name + )) + if(!get_rank_details.warn_execute()) + qdel(get_rank_details) + return + + var/rank_id + var/rank_permissions + if(get_rank_details.NextRow()) + rank_id = get_rank_details.item[1] + rank_permissions = get_rank_details.item[2] + qdel(get_rank_details) + if(!rank_id) + to_chat(usr, "No rank named [rank_name] found!") + return + + if(!isnum(permission_bit) || permission_bit < 1 || floor(permission_bit) != permission_bit) + to_chat(usr, "Provided permission '[permission_bit]' is not positive whole number!") + return + + var/pow2 = round(log(2, permission_bit), 1) + if((2 ** pow2) != permission_bit) + to_chat(usr, "Provided permission '[permission_bit]' is not a power of two, and would affect multiple permission bits!") + return + + if(rank_permissions & permission_bit) + // Revoke the permission. + var/datum/db_query/create_removal = SSdbcore.NewQuery("UPDATE admin_ranks SET default_permissions = default_permissions & ~:permission_bit WHERE id = :rank_id", list( + "permission_bit" = permission_bit, + "rank_id" = rank_id + )) + if(!create_removal.warn_execute()) + qdel(create_removal) + return + qdel(create_removal) + message_admins("Admin ranks updated by [usr.ckey]: [rank_name] no longer has [rights2text(permission_bit)]. An admin reload is required to apply this change.") + + var/logtxt = "Removed permission [rights2text(permission_bit)] (flag = [permission_bit]) from admin rank [rank_name]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + return + + // Grant the permission + var/datum/db_query/grant_permission = SSdbcore.NewQuery("UPDATE admin_ranks SET default_permissions = default_permissions | :permission_bit WHERE id = :rank_id", list( + "permission_bit" = permission_bit, + "rank_id" = rank_id + )) + if(!grant_permission.warn_execute()) + qdel(grant_permission) + return + qdel(grant_permission) + message_admins("Admin ranks updated by [usr.ckey]: [rank_name] has been given [rights2text(permission_bit)]. An admin reload is required to apply this change.") + + var/logtxt = "Added permission [rights2text(permission_bit)] (flag = [permission_bit]) to admin rank [rank_name]" + var/datum/db_query/create_log = SSdbcore.NewQuery({" + INSERT INTO admin_log (`datetime` ,`adminckey` ,`adminip` ,`log`) + VALUES (Now() , :uckey, :uip, :logtxt)"}, list( + "uckey" = usr.ckey, + "uip" = usr.client.address, + "logtxt" = logtxt + )) + create_log.warn_execute() + qdel(create_log) + +/datum/admins/proc/get_db_ranks() + if(!db_available()) + return + var/datum/db_query/get_ranks = SSdbcore.NewQuery("SELECT name FROM admin_ranks") + if(!get_ranks.warn_execute()) + qdel(get_ranks) + CRASH("Unable to get admin ranks from database.") + + var/list/ranks = list() + while(get_ranks.NextRow()) + ranks += get_ranks.item[1] + qdel(get_ranks) + + return ranks + +/datum/admins/proc/get_db_rank_id(rank_name) + if(!db_available()) + return + var/datum/db_query/get_rank_id = SSdbcore.NewQuery("SELECT id FROM admin_ranks WHERE name = :rank_name", list( + "rank_name" = rank_name + )) + if(!get_rank_id.warn_execute()) + qdel(get_rank_id) + return + + var/rank_id + if(get_rank_id.NextRow()) + rank_id = get_rank_id.item[1] + qdel(get_rank_id) + return rank_id + +/datum/admins/proc/get_db_rank_permissions(rank_name) + if(!db_available()) + return + var/datum/db_query/get_rank_permissions = SSdbcore.NewQuery("SELECT default_permissions FROM admin_ranks WHERE name = :rank_name", list( + "rank_name" = rank_name + )) + if(!get_rank_permissions.warn_execute()) + qdel(get_rank_permissions) + return + + var/rank_permissions + if(get_rank_permissions.NextRow()) + rank_permissions = get_rank_permissions.item[1] + qdel(get_rank_permissions) + return rank_permissions diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index fd347bc9d52..8d37e5ec603 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -242,6 +242,44 @@ DB_ban_record(bantype, playermob, banduration, banreason, banjob, null, banckey, banip, bancid) + else if(href_list["editrank"]) + if(!check_rights(R_PERMISSIONS)) + message_admins("[key_name_admin(usr)] attempted to edit the admin permissions without sufficient rights.") + log_admin("[key_name(usr)] attempted to edit the admin permissions without sufficient rights.") + return + + var/rank = input("Please select a rank", "Rank to edit", null, null) as null|anything in (get_db_ranks() | "*New Rank*") + if(!rank) + return + if(rank == "*New Rank*") + rank = input("Please input a new rank", "New custom rank", null, null) as null|text + if(!rank) + return + var/rank_id = create_db_rank(rank) + if(!rank_id) + to_chat(usr, "A rank named [rank] already esists.") + return + + while(TRUE) + var/action = input("What do you want to do to [rank]?", "Editing rank [rank]", null, null) as null|anything in list("List Permissions", "Toggle Permission", "Delete Rank (must be unused)") + if(!action) + return + if(action == "List Permissions") + to_chat(usr, "Rank [rank] has the permissions: [rights2text(get_db_rank_permissions(rank), " ")]") + continue + if(action == "Toggle Permission") + var/list/permissionlist = list() + for(var/i=1, i<=R_MAXPERMISSION, i<<=1) //that <<= is shorthand for i = i << 1. Which is a left bitshift + permissionlist[rights2text(i)] = i + var/permission = input("Select a permission to turn on/off", "Editing rank [rank]", null, null) as null|anything in permissionlist + if(!permission) + continue + toggle_db_rank_permission(rank, permissionlist[permission]) + edit_admin_permissions() + continue + delete_db_rank(rank) + return + else if(href_list["editrights"]) if(!check_rights(R_PERMISSIONS)) message_admins("[key_name_admin(usr)] attempted to edit the admin permissions without sufficient rights.") @@ -253,8 +291,10 @@ var/task = href_list["editrights"] if(task == "add") var/new_ckey = ckey(clean_input("New admin's ckey","Admin ckey", null)) - if(!new_ckey) return - if(new_ckey in GLOB.admin_datums) + if(!new_ckey) + return + var/datum/admins/existing_datum = GLOB.admin_datums[new_ckey] + if(existing_datum && existing_datum.rank != "!LOCALHOST!") to_chat(usr, "Error: Topic 'editrights': [new_ckey] is already an admin") return adm_ckey = new_ckey @@ -268,63 +308,90 @@ var/datum/admins/D = GLOB.admin_datums[adm_ckey] if(task == "remove") - if(alert("Are you sure you want to remove [adm_ckey]?","Message","Yes","Cancel") == "Yes") - if(!D) return + if(alert("Are you sure you want to remove [adm_ckey]'s admin rank?","Message","Yes","Cancel") == "Yes") + if(!GLOB.configuration.admin.use_database_admins && !D) + return GLOB.admin_datums -= adm_ckey - D.disassociate() + if(D) + qdel(D) - updateranktodb(adm_ckey, "player") + if(GLOB.configuration.admin.use_database_admins) + var/clear_custom_permissions = alert("Do you also want to clear any custom permissions [adm_ckey] may have?","Message","Yes","No") == "Yes" + remove_db_admin(adm_ckey, clear_custom_permissions) + reload_one_admin(adm_ckey) + edit_admin_permissions() + return message_admins("[key_name_admin(usr)] removed [adm_ckey] from the admins list") log_admin("[key_name(usr)] removed [adm_ckey] from the admins list") - log_admin_rank_modification(adm_ckey, "Removed") else if(task == "rank") var/new_rank - if(length(GLOB.admin_ranks)) - new_rank = input("Please select a rank", "New rank", null, null) as null|anything in (GLOB.admin_ranks|"*New Rank*") + var/display_rank + if(GLOB.configuration.admin.use_database_admins) + new_rank = input("Please select a rank", "New rank", null, null) as null|anything in (get_db_ranks() | "*New Rank*") + else if(length(GLOB.admin_ranks)) + new_rank = input("Please select a rank", "New rank", null, null) as null|anything in (GLOB.admin_ranks | "*New Rank*") else new_rank = input("Please select a rank", "New rank", null, null) as null|anything in list("Mentor", "Trial Admin", "Game Admin", "Developer", "*New Rank*") var/rights = 0 if(D) rights = D.rights + var/rank_id switch(new_rank) if(null,"") return if("*New Rank*") - new_rank = input("Please input a new rank", "New custom rank", null, null) as null|text + new_rank = input("Please input a new rank", "Admin rank for [adm_ckey]", null, null) as null|text if(!GLOB.configuration.admin.use_database_admins) new_rank = ckeyEx(new_rank) if(!new_rank) to_chat(usr, "Error: Topic 'editrights': Invalid rank") return - if(!GLOB.configuration.admin.use_database_admins) + if(GLOB.configuration.admin.use_database_admins) + rank_id = create_db_rank(new_rank) + if(!rank_id) + to_chat(usr, "A rank named [new_rank] already esists.") + return + display_rank = input("Give them a custom title?", "Admin rank for [adm_ckey]", null, null) as null|text + else if(length(GLOB.admin_ranks)) if(new_rank in GLOB.admin_ranks) rights = GLOB.admin_ranks[new_rank] //we typed a rank which already exists, use its rights else GLOB.admin_ranks[new_rank] = 0 //add the new rank to admin_ranks else - if(!GLOB.configuration.admin.use_database_admins) + if(GLOB.configuration.admin.use_database_admins) + rank_id = get_db_rank_id(new_rank) + if(!rank_id) + to_chat(usr, "Rank '[new_rank] not found.") + return + display_rank = input("Give them a custom title?", "Admin rank for [adm_ckey]", null, null) as null|text + else new_rank = ckeyEx(new_rank) rights = GLOB.admin_ranks[new_rank] //we input an existing rank, use its rights + if(GLOB.configuration.admin.use_database_admins) + var/clear_custom_permissions = alert("Do you also want to remove any custom permissions [adm_ckey] may have?","Message","Yes","No") == "Yes" + if(display_rank == "") + display_rank = null + set_db_rank(adm_ckey, rank_id, display_rank, clear_custom_permissions) + reload_one_admin(adm_ckey) + edit_admin_permissions() + return + if(D) - D.disassociate() //remove adminverbs and unlink from client - D.rank = new_rank //update the rank - D.rights = rights //update the rights based on admin_ranks (default: 0) - else - D = new /datum/admins(new_rank, rights, adm_ckey) + qdel(D) + D = new /datum/admins(new_rank, rights, adm_ckey) var/client/C = GLOB.directory[adm_ckey] //find the client with the specified ckey (if they are logged in) D.associate(C) //link up with the client and add verbs - updateranktodb(adm_ckey, new_rank) message_admins("[key_name_admin(usr)] edited the admin rank of [adm_ckey] to [new_rank]") log_admin("[key_name(usr)] edited the admin rank of [adm_ckey] to [new_rank]") - log_admin_rank_modification(adm_ckey, new_rank) else if(task == "permissions") - if(!D) return + if(!D && !GLOB.configuration.admin.use_database_admins) + return while(TRUE) var/list/permissionlist = list() for(var/i=1, i<=R_MAXPERMISSION, i<<=1) //that <<= is shorthand for i = i << 1. Which is a left bitshift @@ -332,6 +399,11 @@ var/new_permission = input("Select a permission to turn on/off", adm_ckey + "'s Permissions", null, null) as null|anything in permissionlist if(!new_permission) return + if(GLOB.configuration.admin.use_database_admins) + toggle_db_permission(adm_ckey, permissionlist[new_permission]) + reload_one_admin(adm_ckey) + edit_admin_permissions() + continue var/oldrights = D.rights var/toggleresult = "ON" D.rights ^= permissionlist[new_permission] @@ -340,7 +412,6 @@ message_admins("[key_name_admin(usr)] toggled the [new_permission] permission of [adm_ckey] to [toggleresult]") log_admin("[key_name(usr)] toggled the [new_permission] permission of [adm_ckey] to [toggleresult]") - log_admin_permission_modification(adm_ckey, permissionlist[new_permission]) edit_admin_permissions() diff --git a/code/modules/client/2fa.dm b/code/modules/client/2fa.dm index a40cffcace3..af9b52c56b9 100644 --- a/code/modules/client/2fa.dm +++ b/code/modules/client/2fa.dm @@ -55,12 +55,15 @@ return // If we are here, they authed successfully - alert(usr, "Congratulations. 2FA is now setup properly for your account. In preferences, you can change whether you want it to ask for a code on every connection or only when your IP changes") B.close() // Default to IP change only prefs._2fa_status = _2FA_ENABLED_IP prefs.save_preferences(src) prefs.ShowChoices(usr) + if(holder && holder.restricted_by_2fa) + reload_one_admin(ckey) + to_chat(usr, "2fa configured, admin verbs enabled.") + alert(usr, "Congratulations. 2FA is now setup properly for your account. In preferences, you can change whether you want it to ask for a code on every connection or only when your IP changes") return @@ -79,6 +82,10 @@ var/confirm = tgui_alert(usr, "Are you SURE you want to deactivate 2FA?", "WARNING", list("Yes", "No")) if(confirm != "Yes") return + if(holder && holder.needs_2fa()) + confirm = tgui_alert(usr, "This will disable most of your admin permissions. Are you REALLY sure?", "WARNING", list("Yes", "No")) + if(confirm != "Yes") + return // Prompt them for a code to make sure they know what they are doing var/entered_code = input(usr, "Please enter a code from your auth app", "2FA Validation") @@ -106,8 +113,13 @@ prefs._2fa_status = _2FA_DISABLED prefs.save_preferences(src) prefs.ShowChoices(usr) + if(holder && holder.needs_2fa()) + reload_one_admin(ckey) alert(usr, "2FA disabled successfully") +/client/proc/has_2fa() + return prefs._2fa_status != _2FA_DISABLED + /datum/preferences/proc/_2fastatus_to_text() switch(_2fa_status) if(_2FA_DISABLED) diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 74ce273a051..d43679372be 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -310,14 +310,9 @@ // Automatically makes localhost connection an admin try_localhost_autoadmin() - holder = GLOB.admin_datums[ckey] - if(holder) - GLOB.admins += src - holder.owner = src - log_client_to_db(tdata) // Make sure our client exists in the DB - // We have a holder. Inform the relevant places + // Holder set up. Inform the relevant places INVOKE_ASYNC(src, PROC_REF(announce_join)) pai_save = new(src) @@ -352,6 +347,12 @@ // ToS accepted tos_consent = TRUE + holder = GLOB.admin_datums[ckey] + if(holder) + holder.associate(src, delay_2fa_complaint = TRUE) + // Must be async because any sleeps (happen in sql queries) will break connecting clients + INVOKE_ASYNC(src, PROC_REF(admin_memo_output), "Show", FALSE, TRUE) + // Setup widescreen view = prefs.viewrange @@ -377,22 +378,6 @@ GLOB.clients += src connection_time = world.time - var/_2fa_alert = FALSE // This is so we can display the message where it will be seen - if(holder) - if(GLOB.configuration.system.is_production && (holder.rights & R_ADMIN) && prefs._2fa_status == _2FA_DISABLED) // If they are an admin and their 2FA is disabled - // No, check_rights() does not work in the above proc, because we dont have a mob yet - _2fa_alert = TRUE - // This also has to be manually done since no mob to use check_rights() on - deadmin() - add_verb(src, /client/proc/readmin) - GLOB.de_admins += ckey - - else - add_admin_verbs() - // Must be async because any sleeps (happen in sql queries) will break connectings clients - INVOKE_ASYNC(src, PROC_REF(admin_memo_output), "Show", FALSE, TRUE) - - // Forcibly enable hardware-accelerated graphics, as we need them for the lighting overlays. // (but turn them off first, since sometimes BYOND doesn't turn them on properly otherwise) spawn(5) // And wait a half-second, since it sounds like you can do this too fast. @@ -466,16 +451,13 @@ if(check_rights(R_ADMIN, FALSE, mob)) // Mob is required. Dont even try without it. to_chat(src, "The queue server is currently [SSqueue.queue_enabled ? "enabled" : "disabled"], with a threshold of [SSqueue.queue_threshold]. This [SSqueue.persist_queue ? "will" : "will not"] persist through rounds.") - if(_2fa_alert) + if(holder && holder.restricted_by_2fa) to_chat(src,"You do not have 2FA enabled. Admin verbs will be unavailable until you have enabled 2FA.") // Very fucking obvious // Tell client about their connection to_chat(src, "You are currently connected [prefs.server_region ? "via the [prefs.server_region] relay" : "directly"] to Paradise.") to_chat(src, "You can change this using the Change Region verb in the OOC tab, as selecting a region closer to you may reduce latency.") display_job_bans(TRUE) - if(check_rights(R_DEBUG|R_VIEWRUNTIMES, FALSE, mob)) - winset(src, "debugmcbutton", "is-disabled=false") - winset(src, "profilecode", "is-disabled=false") /client/proc/is_connecting_from_localhost() var/static/list/localhost_addresses = list("127.0.0.1", "::1") @@ -605,13 +587,9 @@ qdel(query) - var/admin_rank = "Player" // Admins don't get slammed by this, I guess - if(holder) - admin_rank = holder.rank - else - if(check_randomizer(connectiontopic)) - return + if(!holder && check_randomizer(connectiontopic)) + return var/client_address = address if(!client_address) // Localhost can sometimes have no address set @@ -625,10 +603,9 @@ return // Return here because if we somehow didnt pull a number from an INT column, EVERYTHING is breaking //Player already identified previously, we need to just update the 'lastseen', 'ip' and 'computer_id' variables - var/datum/db_query/query_update = SSdbcore.NewQuery("UPDATE player SET lastseen=NOW(), ip=:sql_ip, computerid=:sql_cid, lastadminrank=:sql_ar WHERE id=:sql_id", list( + var/datum/db_query/query_update = SSdbcore.NewQuery("UPDATE player SET lastseen=NOW(), ip=:sql_ip, computerid=:sql_cid WHERE id=:sql_id", list( "sql_ip" = client_address, "sql_cid" = computer_id, - "sql_ar" = admin_rank, "sql_id" = sql_id )) @@ -642,11 +619,10 @@ else //New player!! Need to insert all the stuff - var/datum/db_query/query_insert = SSdbcore.NewQuery("INSERT INTO player (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, :ckey, Now(), Now(), :ip, :cid, :rank)", list( + var/datum/db_query/query_insert = SSdbcore.NewQuery("INSERT INTO player (id, ckey, firstseen, lastseen, ip, computerid) VALUES (null, :ckey, Now(), Now(), :ip, :cid)", list( "ckey" = ckey, "ip" = client_address, "cid" = computer_id, - "rank" = admin_rank )) if(!query_insert.warn_execute()) qdel(query_insert) @@ -1141,8 +1117,22 @@ SSambience.ambience_listening_clients -= src /client/proc/try_localhost_autoadmin() - if(GLOB.configuration.admin.enable_localhost_autoadmin && is_connecting_from_localhost()) - return new /datum/admins("!LOCALHOST!", R_HOST, ckey) + if(!GLOB.configuration.admin.enable_localhost_autoadmin) + return FALSE + if(!is_connecting_from_localhost()) + if(holder && holder.is_localhost_autoadmin) + qdel(holder) + return FALSE + + // Unhook the old admin datum, if any. + if(holder) + qdel(holder) + + // Hook up a new localhost admin datum. + var/datum/admins/admin_datum = new /datum/admins("!LOCALHOST!", R_HOST, ckey) + admin_datum.is_localhost_autoadmin = TRUE + admin_datum.associate(src) + return TRUE // Verb scoped to the client level so its ALWAYS available /client/verb/open_tos() diff --git a/code/modules/tgui/states/admin_state.dm b/code/modules/tgui/states/admin_state.dm index 61fc3731188..d03595a1ce2 100644 --- a/code/modules/tgui/states/admin_state.dm +++ b/code/modules/tgui/states/admin_state.dm @@ -7,6 +7,6 @@ GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new) /datum/ui_state/admin_state/can_use_topic(src_object, mob/user) - if(check_rights_for(user.client, R_ADMIN)) + if(check_rights_client(R_ADMIN, FALSE, user.client)) return UI_INTERACTIVE return UI_CLOSE diff --git a/config/example/config.toml b/config/example/config.toml index 5e2a5611f1b..cb91700c678 100644 --- a/config/example/config.toml +++ b/config/example/config.toml @@ -173,7 +173,7 @@ ipc_screens = [ # Enable/disable the database on a whole sql_enabled = false # SQL version. If this is a mismatch, round start will be delayed -sql_version = 69 +sql_version = 70 # SQL server address. Can be an IP or DNS name sql_address = "127.0.0.1" # SQL server port diff --git a/html/panels.css b/html/panels.css index 0d1cb0cb9fb..3385d663ca3 100644 --- a/html/panels.css +++ b/html/panels.css @@ -1,7 +1,10 @@ body {padding:0px;margin:0px;} #top {position:fixed;top:5px;left:10%;width:80%;text-align:center;background-color:#fff;border:2px solid #ccc;} -#main {position:relative;top:50px;left:3%;width:96%;text-align:center;z-index:0;} +#main {left:3%;width:96%;text-align:center;z-index:0;} #searchable {table-layout:fixed;width:100%;text-align:center;"#f4f4f4";} +#spacer {height:50px} +#db_notice {width:80%;margin-left:10%;margin-right:10%;text-align:center} +#rank_edit {width:100%;text-align:center} tr.norm {background-color:#f4f4f4;} tr.title {background-color:#ccc;} tr.alt {background-color:#e7e7e7;}