Files
Bubberstation/code/modules/admin/permissionedit.dm
LemonInTheDark f2db304b6c Adds In Game Rank Editing (Permissions Panel Cleanup) (#91873)
## About The Pull Request

Ok there's a lot here, sorry bout that.

- Cleaned up the permissions panel backend pretty signficantly
- Added some extra security measures to said code, mostly proc call
checks
- Properly implemented filtering code jordie wrote years and years ago
for permissions logs
- Cleaned up the permissions ui generally, more bars, nicer lookin
stuff, etc
- Fixed the Management panel's relationship with combined roles, and
renamed it to Housekeeping. Its display is expanded too.
- Added tracking to rank datums on where exactly they came from
- Added a new tab to the permissions panel which allows the modification
and deletion of ranks
- Beefed up rank modification to try and avoid accidential temp rank
additions to the db

I'm doing my best to avoid perms escalation issues, tho they are always
possible right.
Also, got mad at some query cleanup handling, did a pass on it. this
isn't nearly all of em, but it's some.

## Why It's Good For The Game

I realized there is no way to, in game, cleanly edit/create ranks, and
that the way the existing system worked was quite opaque.
I'm trying to fix that here. It does mean potentially opening up DB rank
deletion/modification to bad actors, but frankly I am not overly worried
about that. Admin modification has always been a vulnerability so like.

Here's a video with my changes (mostly, it's lightly outdated)
https://file.house/XqME7KWKk0ULj4ZUkJ5reg==.mp4

## Changelog
🆑
refactor: Fucked with admin rank setup very slightly, please yell at me
if anything is wrong.
admin: Updated the permissions panel to be a good bit more user
friendly, added rank management support to it.
server: I've added code that gives the game modification/deletion perms
for the rank table, be made aware.
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
2025-07-04 16:33:06 -04:00

1216 lines
50 KiB
Plaintext

ADMIN_VERB(edit_admin_permissions, R_PERMISSIONS, "Permissions Panel", "Edit admin permissions.", ADMIN_CATEGORY_MAIN)
user.holder.edit_admin_permissions(PERMISSIONS_PAGE_PERMISSIONS)
#define PERMISSIONS_LOGS_PER_PAGE 20
/// List of all the actions you have ever been able to take in admin logs, keep in parity with the
/// operation enum in the admin_log table
GLOBAL_LIST_INIT(permission_action_types, list(
PERMISSIONS_ACTION_ADMIN_ADDED,
PERMISSIONS_ACTION_ADMIN_REMOVED,
PERMISSIONS_ACTION_ADMIN_RANK_CHANGED,
PERMISSIONS_ACTION_RANK_ADDED,
PERMISSIONS_ACTION_RANK_REMOVED,
PERMISSIONS_ACTION_RANK_CHANGED,
PERMISSIONS_ACTION_NONE
))
/datum/admins/proc/edit_admin_permissions(action, log_target, log_actor, log_operation, log_page)
if(!check_rights(R_PERMISSIONS))
return
var/datum/asset/asset_cache_datum = get_asset_datum(/datum/asset/group/permissions)
asset_cache_datum.send(usr)
var/list/output = list("<link rel='stylesheet' type='text/css' href='[SSassets.transport.get_asset_url("panels.css")]'>")
var/new_line = " | "
if(action == PERMISSIONS_PAGE_PERMISSIONS)
new_line = "<br>"
output += {"
<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowser=1'>\
[action == PERMISSIONS_PAGE_PERMISSIONS ? "<b>\[Permissions\]</b": "\[Permissions\]"]
</a>
[new_line]<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserranks=1'>
[action == PERMISSIONS_PAGE_RANKS ? "<b>\[Ranks\]</b": "\[Ranks\]"]
</a>
[new_line]<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserlogging=1;editrightslogpage=0'>
[action == PERMISSIONS_PAGE_LOGGING ? "<b>\[Logging\]</b": "\[Logging\]"]
</a>
[new_line]<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserhousekeep=1'>
[action == PERMISSIONS_PAGE_HOUSEKEEPING ? "<b>\[Housekeeping\]</b>": "\[Housekeeping\]"]
</a>
"}
if(action != PERMISSIONS_PAGE_PERMISSIONS)
output += "<hr style='background:#000000; border:0; height:3px'>"
if(action == PERMISSIONS_PAGE_PERMISSIONS)
output += {"
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Permissions Panel</title>
<script type='text/javascript' src='[SSassets.transport.get_asset_url("search.js")]'></script>
</head>
<body onload='selectTextField();updateSearch();'>
<div id='main'>
<table id='searchable' cellspacing='0'>
<tr class='title'>
<th style='width:150px;'>CKEY <a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=add'>\[+\]</a></th>
<th style='width:125px;'>RANK</th>
<th>PERMISSIONS</th>
</tr>
"}
for(var/admin_ckey in GLOB.admin_datums + GLOB.deadmins)
var/datum/admins/admin_datum = GLOB.admin_datums[admin_ckey]
if(!admin_datum)
admin_datum = GLOB.deadmins[admin_ckey]
if (!admin_datum)
continue
if(admin_datum.owner)
admin_ckey = admin_datum.owner.key
var/deadmin_link = ""
if (admin_datum.deadmined)
deadmin_link = "<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=activate;key=[admin_ckey]'>\[RA\]</a>"
else
deadmin_link = "<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=deactivate;key=[admin_ckey]'>\[DA\]</a>"
var/verify_link = ""
if (admin_datum.blocked_by_2fa)
verify_link = "<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=verify;key=[admin_ckey]'>\[2FA VERIFY\]</a>"
output += {"
<tr>
<td style='text-align:center;'>[admin_ckey]
<br> [deadmin_link]
<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=remove;key=[admin_ckey]'>\[-\]</a>
<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=sync;key=[admin_ckey]'>\[SYNC TGDB\]</a>
[verify_link]
</td>
<td>
<a href='byond://?src=[REF(src)];[HrefToken()];editrights=rank;key=[admin_ckey]'>[admin_datum.rank_names()]</a>
</td>
<td>
<a class='small' href='byond://?src=[REF(src)];[HrefToken()];editrights=permissions;key=[admin_ckey]'>[rights2text(admin_datum.rank_flags(), " ")]</a>
</td>
</tr>
"}
output += {"
</table>
</div>
<div id='top'>
<b>Search:</b>
<input type='text' id='filter' value='' style='width:70%;' onkeyup='updateSearch();'>
</div>
</body>
"}
else if(action == PERMISSIONS_PAGE_RANKS)
var/creation_meaningful = check_rights(R_PERMISSIONS) && usr.client.holder.can_edit_rights_flags() != NONE
output += {"
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Permissions Panel</title>
</head>
[creation_meaningful ? "<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserranks=1;editrightsaddrank=1'>\[Create Rank\]</a>" : ""]
<hr style='background:#000000; border:0; height:3px'>
"}
// First we're gonna collect admins by rank, for sorting purposes
var/datum/db_query/query_extract_admins = SSdbcore.NewQuery("SELECT IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("admin")].ckey), ckey), [format_table_name("admin")].`rank` FROM [format_table_name("admin")]")
if(!query_extract_admins.warn_execute())
qdel(query_extract_admins)
return
var/list/admins_by_rank = list()
while(query_extract_admins.NextRow())
var/admin_key = query_extract_admins.item[1]
var/admin_rank = query_extract_admins.item[2]
for(var/datum/admin_rank/composed_rank as anything in ranks_from_rank_name(admin_rank))
admins_by_rank[composed_rank.name] ||= list()
admins_by_rank[composed_rank.name] |= list(admin_key)
QDEL_NULL(query_extract_admins)
for(var/stored_key in GLOB.admin_datums)
var/datum/admins/live_holder = GLOB.admin_datums[stored_key]
for(var/datum/admin_rank/composed_rank as anything in live_holder.ranks)
admins_by_rank[composed_rank.name] ||= list()
admins_by_rank[composed_rank.name] |= list(stored_key)
// Then, pull the full list of DB ranks
var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
if(!query_extract_ranks.warn_execute())
qdel(query_extract_ranks)
return
var/list/all_ranks = list()
while(query_extract_ranks.NextRow())
var/loaded_rank = query_extract_ranks.item[1]
var/rank_flags = text2num(query_extract_ranks.item[2])
var/rank_exclude_flags = text2num(query_extract_ranks.item[3])
var/rank_can_edit_flags = text2num(query_extract_ranks.item[4])
all_ranks[loaded_rank] = list("rank" = loaded_rank, "flags" = rank_flags, "exclude_flags" = rank_exclude_flags, "can_edit_flags" = rank_can_edit_flags)
QDEL_NULL(query_extract_ranks)
for(var/datum/admin_rank/rank as anything in GLOB.admin_ranks)
// This does technically potentially mask local edits? to db ranks if that is even possible (should not be), but I prefer to be honest about longterm values.
if(all_ranks[rank.name])
continue
all_ranks[rank.name] = list("rank" = rank.name, "flags" = rank.include_rights, "exclude_flags" = rank.exclude_rights, "can_edit_flags" = rank.can_edit_rights)
sortTim(all_ranks, GLOBAL_PROC_REF(cmp_text_asc))
for(var/rank_name in all_ranks)
var/list/datum/admin_rank/rank_datums = ranks_from_rank_name(rank_name)
if(!length(rank_datums))
continue
var/datum/admin_rank/rank_datum = rank_datums[1]
var/list/rank_info = all_ranks[rank_name]
var/permissions = rights2text(rank_info["flags"], seperator = " ", prefix = "+")
var/denied_permissions = rights2text(rank_info["exclude_flags"], seperator = " ", prefix = "-")
var/editing_permissions = rights2text(rank_info["can_edit_flags"], seperator = " ", prefix = "*")
var/modify_might_be_allowed = FALSE
var/delete_might_be_allowed = FALSE
if((rank_datum.source == RANK_SOURCE_DB && check_rights(R_DBRANKS)) || rank_datum.source == RANK_SOURCE_TEMPORARY)
modify_might_be_allowed = TRUE
delete_might_be_allowed = TRUE
if(length(admins_by_rank[rank_name]) != 0)
delete_might_be_allowed = FALSE
if((usr.client.holder.can_edit_rights_flags() & rank_datum.rights) != rank_datum.rights)
modify_might_be_allowed = FALSE
delete_might_be_allowed = FALSE
output += {"
<b>[rank_name]</b>
[modify_might_be_allowed ? " | <a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserranks=1;editrightseditrank=[rank_name]'>\[Edit\]</a>" : ""]
[delete_might_be_allowed ? " | <a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserranks=1;editrightsremoverank=[rank_name]'>\[Delete\]</a>" : ""]
<br>Source: [rank_datum.pretty_print_source()]
<br>Held By: [length(admins_by_rank[rank_name])]
<br>Permissions: [permissions]
<br>Denied: [denied_permissions]
<br>Allowed to edit: [editing_permissions]
<hr style='background:#000000; border:0; height:1px'>
"}
else if(action == PERMISSIONS_PAGE_LOGGING)
output += {"
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Permissions Panel</title>
</head>
"}
var/log_count = 0
var/page_count = 0
var/selected_page = 0
log_target ||= ""
log_actor ||= ""
log_operation ||= null
if(log_operation == PERMISSIONS_ACTION_NONE)
log_operation = null
if(log_page)
selected_page = text2num(log_page)
var/list/action_options = list()
for(var/action_type in GLOB.permission_action_types)
if(action_type == log_operation || (isnull(log_operation) && action_type == PERMISSIONS_ACTION_NONE))
action_options += "<option value='[action_type]' selected>[action_type]</option>"
else
action_options += "<option value='[action_type]'>[action_type]</option>"
// We intentionally do not carry page values through searches since they'd get messed with
output += {"
<form method='get' action='?'>
[HrefTokenFormField()]
<input type='hidden' name='_src_' value='holder'>
<input type='hidden' name='editrightsbrowserlogging' value='1'>
<b>Ckey Modified: </b>
<input type='text' name='editrightslogtarget' value='[log_target]' style='width:20%;''>
<b> Acting Admin: </b>
<input type='text' name='editrightslogactor' value='[log_actor]' style='width:20%;''>
<b> Action: </b>
<select name="editrightslogoperation" style='width:20%;'>
[action_options.Join()]
</select>
<input type='submit' value='Search'>
</form>
"}
var/datum/db_query/query_count_admin_logs = SSdbcore.NewQuery({"
SELECT COUNT(id) FROM [format_table_name("admin_log")]
WHERE target LIKE CONCAT('%',:target,'%')
AND adminckey LIKE CONCAT('%',:adminckey,'%')
AND (:operation IS NULL OR operation = :operation)
"},
list("target" = log_target, "adminckey" = log_actor, "operation" = log_operation)
)
if(!query_count_admin_logs.warn_execute())
qdel(query_count_admin_logs)
return
if(query_count_admin_logs.NextRow())
log_count = text2num(query_count_admin_logs.item[1])
QDEL_NULL(query_count_admin_logs)
if(log_count > PERMISSIONS_LOGS_PER_PAGE)
output += "<b>Page: </b>"
while(log_count > 0)
output += {"
|<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserlogging=1;editrightslogtarget=[log_target];editrightslogactor=[log_actor];editrightslogoperation=[log_operation];editrightslogpage=[page_count]'>
[page_count == selected_page ? "<b>\[[page_count]\]</b>" : "\[[page_count]\]"]
</a>
"}
log_count -= PERMISSIONS_LOGS_PER_PAGE
page_count++
output += "|"
var/datum/db_query/query_search_admin_logs = SSdbcore.NewQuery({"
SELECT
datetime,
round_id,
IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
operation,
IF(ckey IS NULL, target, byond_key),
log
FROM [format_table_name("admin_log")]
LEFT JOIN [format_table_name("player")] ON target = ckey
WHERE target LIKE CONCAT('%',:target,'%')
AND adminckey LIKE CONCAT('%',:adminckey,'%')
AND (:operation IS NULL OR operation = :operation)
ORDER BY datetime DESC
LIMIT :skip, :take
"}, list("target" = log_target, "adminckey" = log_actor, "operation" = log_operation, "skip" = PERMISSIONS_LOGS_PER_PAGE * selected_page, "take" = PERMISSIONS_LOGS_PER_PAGE))
if(!query_search_admin_logs.warn_execute())
qdel(query_search_admin_logs)
return
var/action_found = FALSE
while(query_search_admin_logs.NextRow())
var/datetime = query_search_admin_logs.item[1]
var/round_id = query_search_admin_logs.item[2]
var/admin_key = query_search_admin_logs.item[3]
var/operation_applied = query_search_admin_logs.item[4]
var/ckey_actioned = query_search_admin_logs.item[5]
var/log = query_search_admin_logs.item[6]
if(!action_found)
action_found = TRUE
output += "<hr style='background:#000000; border:0; height:3px'>"
output += {"
<p style='margin:0px'>
<b>[datetime] | Round ID [round_id] | Admin [admin_key] | Operation [operation_applied] on [ckey_actioned]</b>
<br>[log]
</p>
<hr style='background:#000000; border:0; height:3px'>
"}
QDEL_NULL(query_search_admin_logs)
else if(action == PERMISSIONS_PAGE_HOUSEKEEPING)
output += {"
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Permissions Panel</title>
</head>
"}
// We're gonna sanity check admin rank info, yea?
// We're pulling from the db as a source of truth here, instead of trusting the local lists
// First, pull all admin ranks which are used
var/datum/db_query/query_extract_admins = SSdbcore.NewQuery("SELECT IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("admin")].ckey), ckey), [format_table_name("admin")].`rank` FROM [format_table_name("admin")]")
if(!query_extract_admins.warn_execute())
qdel(query_extract_admins)
return
var/list/admins_by_rank = list()
while(query_extract_admins.NextRow())
var/admin_key = query_extract_admins.item[1]
var/admin_rank = query_extract_admins.item[2]
for(var/datum/admin_rank/composed_rank as anything in ranks_from_rank_name(admin_rank))
admins_by_rank[composed_rank.name] += list(admin_key)
QDEL_NULL(query_extract_admins)
// Then, pull the full list of DB ranks to purity check against
var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
if(!query_extract_ranks.warn_execute())
qdel(query_extract_ranks)
return
var/list/all_db_ranks = list()
while(query_extract_ranks.NextRow())
var/loaded_rank = query_extract_ranks.item[1]
var/rank_flags = query_extract_ranks.item[2]
var/rank_exclude_flags = query_extract_ranks.item[3]
var/rank_can_edit_flags = query_extract_ranks.item[4]
all_db_ranks[loaded_rank] = list("rank" = loaded_rank, "flags" = rank_flags, "exclude_flags" = rank_exclude_flags, "can_edit_flags" = rank_can_edit_flags)
QDEL_NULL(query_extract_ranks)
output += "<h3>Admin ckeys with invalid ranks</h3>"
var/list/invalid_admin_ranks = admins_by_rank - all_db_ranks
if(length(invalid_admin_ranks)) // Add a nice leader to the output
output += "<hr style='background:#000000; border:0; height:1px'>"
else
output += "No invalid ranks found."
for(var/illegal_rank in invalid_admin_ranks)
for(var/misapplied_admin in admins_by_rank[illegal_rank])
output += {"
[misapplied_admin] has the non-existent rank [illegal_rank] |
<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserhousekeep=1;editrightschange=[misapplied_admin]'>\[Change Rank\]</a> |
<a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserhousekeep=1;editrightsremove=[misapplied_admin]'>\[Remove\]</a>
<hr style='background:#000000; border:0; height:1px'>
"}
output += "<h3>Unused DB Ranks</h3>"
var/list/unused_ranks = all_db_ranks - admins_by_rank
if(length(unused_ranks))
output += "<hr style='background:#000000; border:0; height:1px'>"
else
output += "No unused ranks found."
sortTim(unused_ranks, GLOBAL_PROC_REF(cmp_text_asc))
for(var/unused_rank in unused_ranks)
var/list/datum/admin_rank/rank_datums = ranks_from_rank_name(unused_rank)
var/datum/admin_rank/rank_datum = rank_datums[1]
var/list/rank_info = all_db_ranks[unused_rank]
var/permissions = rights2text(text2num(rank_info["flags"]), seperator = " ", prefix = "+")
var/denied_permissions = rights2text(text2num(rank_info["exclude_flags"]), seperator = " ", prefix = "-")
var/editing_permissions = rights2text(text2num(rank_info["can_edit_flags"]), seperator = " ", prefix = "*")
var/delete_might_be_allowed = FALSE
// First case should never fail, but uh, just in case
if(rank_datum.source == RANK_SOURCE_DB && check_rights(R_DBRANKS))
delete_might_be_allowed = TRUE
if((usr.client.holder.can_edit_rights_flags() & rank_datum.rights) == rank_datum.rights)
delete_might_be_allowed = FALSE
output += {"
Rank [unused_rank] is not held by any admin
[delete_might_be_allowed ? " | <a href='byond://?_src_=holder;[HrefToken()];editrightsbrowserhousekeep=1;editrightsremoverank=[unused_rank]'>\[Delete\]</a>" : ""]
<br>Source: [rank_datum.pretty_print_source()]
<br>Permissions: [permissions]
<br>Denied: [denied_permissions]
<br>Allowed to edit: [editing_permissions]
<hr style='background:#000000; border:0; height:1px'>
"}
if(QDELETED(usr))
return
usr << browse("<!DOCTYPE html><html>[jointext(output, "")]</html>","window=editrights;size=1000x650")
/datum/admins/proc/edit_rights_topic(list/href_list)
if(!check_rights(R_PERMISSIONS))
message_admins("[key_name_admin(usr)] attempted to edit admin permissions without sufficient rights.")
log_admin("[key_name(usr)] attempted to edit admin permissions without sufficient rights.")
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Admin Edit blocked: Advanced ProcCall detected."), confidential = TRUE)
return
var/datum/asset/permissions_assets = get_asset_datum(/datum/asset/simple/namespaced/common)
permissions_assets.send(usr.client)
var/admin_key = href_list["key"]
var/admin_ckey = ckey(admin_key)
var/task = href_list["editrights"]
var/datum/admins/target_admin_datum = GLOB.admin_datums[admin_ckey]
if(!target_admin_datum)
target_admin_datum = GLOB.deadmins[admin_ckey]
if (!target_admin_datum && task != "add")
return
var/use_db
var/skip
var/legacy_only
if(task == "activate" || task == "deactivate" || task == "sync" || task == "verify" || task == "permissions")
skip = TRUE
if(!CONFIG_GET(flag/admin_legacy_system) && CONFIG_GET(flag/protect_legacy_admins) && task == "rank")
if(admin_ckey in GLOB.protected_admins)
to_chat(usr, span_adminprefix("Editing the rank of this admin is blocked by server configuration."), confidential = TRUE)
return
if(!CONFIG_GET(flag/admin_legacy_system) && CONFIG_GET(flag/protect_legacy_ranks) && task == "permissions")
if((target_admin_datum.ranks & GLOB.protected_ranks).len > 0)
to_chat(usr, span_adminprefix("Editing the flags of this rank is blocked by server configuration."), confidential = TRUE)
return
if(CONFIG_GET(flag/load_legacy_ranks_only) && (task == "add" || task == "rank" || task == "permissions"))
to_chat(usr, span_adminprefix("Database rank loading is disabled, only temporary changes can be made to a rank's permissions and permanently creating a new rank is blocked."), confidential = TRUE)
legacy_only = TRUE
if(check_rights(R_DBRANKS, FALSE) && !skip)
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Unable to connect to database, changes are temporary only."), confidential = TRUE)
use_db = FALSE
else
use_db = tgui_alert(usr,"Permanent changes are saved to the database for future rounds, temporary changes will affect only the current round", "Permanent or Temporary?", list("Permanent", "Temporary", "Cancel"))
if(isnull(use_db) || use_db == "Cancel")
return
if(use_db == "Permanent")
use_db = TRUE
else
use_db = FALSE
if(QDELETED(usr))
return
if(target_admin_datum && (task != "sync" && task != "verify") && !check_if_greater_rights_than_holder(target_admin_datum))
message_admins("[key_name_admin(usr)] attempted to change the rank of [admin_key] without sufficient rights.")
log_admin("[key_name(usr)] attempted to change the rank of [admin_key] without sufficient rights.")
return
switch(task)
if("add")
admin_ckey = add_admin(admin_ckey, admin_key, use_db)
if(!admin_ckey)
return
if(!admin_key) // Prevents failures in logging admin rank changes.
admin_key = admin_ckey
change_admin_rank(admin_ckey, admin_key, use_db, null, legacy_only)
if("remove")
remove_admin(admin_ckey, admin_key, use_db, target_admin_datum)
if("rank")
change_admin_rank(admin_ckey, admin_key, use_db, target_admin_datum, legacy_only)
if("permissions")
change_admin_flags(admin_ckey, admin_key, target_admin_datum)
if("activate")
force_readmin(admin_key, target_admin_datum)
if("deactivate")
force_deadmin(admin_key, target_admin_datum)
if("sync")
sync_lastadminrank(admin_ckey, admin_key, target_admin_datum)
if("verify")
var/msg = "has authenticated [admin_ckey]"
message_admins("[key_name_admin(usr)] [msg]")
log_admin("[key_name(usr)] [msg]")
target_admin_datum.bypass_2fa = TRUE
target_admin_datum.associate(GLOB.directory[admin_ckey])
edit_admin_permissions(PERMISSIONS_PAGE_PERMISSIONS)
/datum/admins/proc/add_admin(admin_ckey, admin_key, use_db)
if(!check_rights(R_PERMISSIONS) || (use_db && !check_rights(R_DBRANKS)))
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Admin Addition blocked: Advanced ProcCall detected."), confidential = TRUE)
return
if(admin_ckey)
. = admin_ckey
else
admin_key = input("New admin's key","Admin key") as text|null
. = ckey(admin_key)
if(!.)
return FALSE
if(!admin_ckey && (. in (GLOB.admin_datums+GLOB.deadmins)))
to_chat(usr, span_danger("[admin_key] is already an admin."), confidential = TRUE)
return FALSE
if(!use_db)
return
//if an admin exists without a datum they won't be caught by the above
var/datum/db_query/query_admin_in_db = SSdbcore.NewQuery(
"SELECT 1 FROM [format_table_name("admin")] WHERE ckey = :ckey",
list("ckey" = .)
)
if(!query_admin_in_db.warn_execute())
qdel(query_admin_in_db)
return FALSE
if(query_admin_in_db.NextRow())
qdel(query_admin_in_db)
to_chat(usr, span_danger("[admin_key] already listed in admin database. Check the Housekeeping tab if they don't appear in the list of admins."), confidential = TRUE)
return FALSE
QDEL_NULL(query_admin_in_db)
var/datum/db_query/query_add_admin = SSdbcore.NewQuery(
"INSERT INTO [format_table_name("admin")] (ckey, `rank`) VALUES (:ckey, 'NEW ADMIN')",
list("ckey" = .)
)
if(!query_add_admin.warn_execute())
qdel(query_add_admin)
return FALSE
QDEL_NULL(query_add_admin)
var/datum/db_query/query_add_admin_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_ADMIN_ADDED]', :target, CONCAT('New admin added: ', :target))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "target" = .))
if(!query_add_admin_log.warn_execute())
qdel(query_add_admin_log)
return
QDEL_NULL(query_add_admin_log)
/datum/admins/proc/remove_admin(admin_ckey, admin_key, use_db, datum/admins/target_holder)
if(!check_rights(R_PERMISSIONS) || (use_db && !check_rights(R_DBRANKS)))
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Admin Removal blocked: Advanced ProcCall detected."), confidential = TRUE)
return
if(tgui_alert(usr,"Are you sure you want to remove [admin_ckey]?", "Confirm Removal", list("Do it", "Cancel")) != "Do it")
return
GLOB.admin_datums -= admin_ckey
GLOB.deadmins -= admin_ckey
if(target_holder)
target_holder.disassociate()
var/m1 = "[key_name_admin(usr)] removed [admin_key] from the admins list [use_db ? "permanently" : "temporarily"]"
var/m2 = "[key_name(usr)] removed [admin_key] from the admins list [use_db ? "permanently" : "temporarily"]"
if(!use_db)
message_admins(m1)
log_admin(m2)
return
var/datum/db_query/query_remove_admin = SSdbcore.NewQuery(
"DELETE FROM [format_table_name("admin")] WHERE ckey = :ckey",
list("ckey" = admin_ckey)
)
if(!query_remove_admin.warn_execute())
qdel(query_remove_admin)
return
QDEL_NULL(query_remove_admin)
message_admins(m1)
log_admin(m2)
var/datum/db_query/query_remove_admin_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_ADMIN_REMOVED]', :admin_ckey, CONCAT('Admin removed: ', :admin_ckey))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "admin_ckey" = admin_ckey))
if(!query_remove_admin_log.warn_execute())
qdel(query_remove_admin_log)
return
QDEL_NULL(query_remove_admin_log)
sync_lastadminrank(admin_ckey, admin_key)
/datum/admins/proc/force_readmin(admin_key, datum/admins/target_holder)
if(!target_holder || !target_holder.deadmined)
return
target_holder.activate()
message_admins("[key_name_admin(usr)] forcefully readmined [admin_key]")
log_admin("[key_name(usr)] forcefully readmined [admin_key]")
/datum/admins/proc/force_deadmin(admin_key, datum/admins/target_holder)
if(!target_holder || target_holder.deadmined)
return
message_admins("[key_name_admin(usr)] forcefully deadmined [admin_key]")
log_admin("[key_name(usr)] forcefully deadmined [admin_key]")
target_holder.deactivate() //after logs so the deadmined admin can see the message.
/datum/admins/proc/auto_deadmin()
if(owner.is_localhost())
return FALSE
if(owner.prefs.read_preference(/datum/preference/toggle/bypass_deadmin_in_centcom) && is_centcom_level(owner.mob.z) && !istype(owner.mob, /mob/dead/new_player))
return FALSE
to_chat(owner, span_interface("You are now a normal player."), confidential = TRUE)
var/old_owner = owner
deactivate()
message_admins("[old_owner] deadmined via auto-deadmin config.")
log_admin("[old_owner] deadmined via auto-deadmin config.")
return TRUE
#define RANK_DONE ":) I'm Done"
/datum/admins/proc/change_admin_rank(admin_ckey, admin_key, use_db, datum/admins/target_holder, legacy_only)
if(!check_rights(R_PERMISSIONS) || (use_db && !check_rights(R_DBRANKS)))
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Rank Modification blocked: Advanced ProcCall detected."), confidential = TRUE)
return
var/rank_type = RANK_SOURCE_TEMPORARY
if(use_db)
rank_type = RANK_SOURCE_DB
var/list/rank_names = list()
if(!use_db || (use_db && !legacy_only))
rank_names += "*New Rank*"
for(var/datum/admin_rank/admin_rank as anything in GLOB.admin_ranks)
if((admin_rank.rights & usr.client.holder.can_edit_rights_flags()) != admin_rank.rights)
continue
if(use_db && admin_rank.source != RANK_SOURCE_DB && admin_rank.source != RANK_SOURCE_TXT)
continue
rank_names[admin_rank.name] = admin_rank
var/list/new_rank_names = list()
var/list/custom_ranks = list()
while (TRUE)
var/list/display_rank_names = list(RANK_DONE)
if (new_rank_names.len > 0)
display_rank_names += "** SELECTED **"
for (var/rank_name in new_rank_names)
display_rank_names += rank_name
display_rank_names += "---------"
for (var/rank_name in rank_names)
if (!(rank_name in display_rank_names))
display_rank_names += rank_name
var/next_rank = input("Please select a rank, or select [RANK_DONE] if you are finished.") as null|anything in display_rank_names
if (isnull(next_rank))
return
if (next_rank == RANK_DONE)
break
// They clicked "** SELECTED **" or something silly.
if (!(next_rank in rank_names))
continue
if (next_rank in new_rank_names)
new_rank_names -= next_rank
continue
if (next_rank == "*New Rank*")
var/new_rank_name = input("Please input a new rank", "New custom rank") as text|null
if (!new_rank_name)
return
var/datum/admin_rank/custom_rank = rank_names[new_rank_name]
if(!isnull(custom_rank))
continue
if (target_holder)
custom_rank = new(new_rank_name, rank_type, target_holder.rank_flags())
else
custom_rank = new(new_rank_name, rank_type)
if(QDELETED(custom_rank))
continue
GLOB.admin_ranks += custom_rank
custom_ranks += custom_rank
new_rank_names += new_rank_name
new_rank_names += next_rank
var/list/new_ranks = list()
for (var/datum/admin_rank/admin_rank as anything in GLOB.admin_ranks)
if (admin_rank.name in new_rank_names)
new_ranks += admin_rank
new_rank_names -= admin_rank.name
if (new_rank_names.len == 0)
break
var/joined_rank = join_admin_ranks(new_ranks)
var/m1 = "[key_name_admin(usr)] edited the admin rank of [admin_key] to [joined_rank] [use_db ? "permanently" : "temporarily"]"
var/m2 = "[key_name(usr)] edited the admin rank of [admin_key] to [joined_rank] [use_db ? "permanently" : "temporarily"]"
if(use_db)
//if a player was tempminned before having a permanent change made to their rank they won't yet be in the db
var/old_rank
var/datum/db_query/query_admin_in_db = SSdbcore.NewQuery(
"SELECT `rank` FROM [format_table_name("admin")] WHERE ckey = :admin_ckey",
list("admin_ckey" = admin_ckey)
)
if(!query_admin_in_db.warn_execute())
qdel(query_admin_in_db)
return
if(!query_admin_in_db.NextRow())
add_admin(admin_ckey, admin_key, TRUE)
old_rank = "NEW ADMIN"
else
old_rank = query_admin_in_db.item[1]
QDEL_NULL(query_admin_in_db)
for (var/datum/admin_rank/custom_rank in custom_ranks)
//similarly if a temp rank is created it won't be in the db if someone is permanently changed to it
var/datum/db_query/query_rank_in_db = SSdbcore.NewQuery(
"SELECT 1 FROM [format_table_name("admin_ranks")] WHERE `rank` = :new_rank",
list("new_rank" = custom_rank.name)
)
if(!query_rank_in_db.warn_execute())
qdel(query_rank_in_db)
return
if(query_rank_in_db.NextRow())
QDEL_NULL(query_rank_in_db)
continue
QDEL_NULL(query_rank_in_db)
var/datum/db_query/query_add_rank = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_ranks")] (`rank`, flags, exclude_flags, can_edit_flags)
VALUES (:new_rank, '0', '0', '0')
"}, list("new_rank" = custom_rank.name))
if(!query_add_rank.warn_execute())
qdel(query_add_rank)
return
QDEL_NULL(query_add_rank)
var/datum/db_query/query_add_rank_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_RANK_ADDED]', :new_rank, CONCAT('New rank added: ', :new_rank))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "new_rank" = custom_rank.name))
if(!query_add_rank_log.warn_execute())
qdel(query_add_rank_log)
return
QDEL_NULL(query_add_rank_log)
var/datum/db_query/query_change_rank = SSdbcore.NewQuery(
"UPDATE [format_table_name("admin")] SET `rank` = :new_rank WHERE ckey = :admin_ckey",
list("new_rank" = joined_rank, "admin_ckey" = admin_ckey)
)
if(!query_change_rank.warn_execute())
qdel(query_change_rank)
return
QDEL_NULL(query_change_rank)
message_admins(m1)
log_admin(m2)
var/datum/db_query/query_change_rank_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_ADMIN_RANK_CHANGED]', :target, CONCAT('Rank of ', :target, ' changed from ', :old_rank, ' to ', :new_rank))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "target" = admin_ckey, "old_rank" = old_rank, "new_rank" = joined_rank))
if(!query_change_rank_log.warn_execute())
qdel(query_change_rank_log)
return
QDEL_NULL(query_change_rank_log)
else
message_admins(m1)
log_admin(m2)
if(target_holder) //they were previously an admin
target_holder.disassociate() //existing admin needs to be disassociated
target_holder.ranks = new_ranks //set the admin_rank as our rank
target_holder.bypass_2fa = TRUE // Another admin has cleared us
var/client/target_client = GLOB.directory[admin_ckey]
target_holder.associate(target_client)
else
target_holder = new(new_ranks, admin_ckey) //new admin
target_holder.bypass_2fa = TRUE // Another admin has cleared us
target_holder.activate()
#undef RANK_DONE
/// Changes, for this round only, the flags a particular admin gets to use
/datum/admins/proc/change_admin_flags(admin_ckey, admin_key, datum/admins/admin_holder)
if(!check_rights(R_PERMISSIONS))
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Rank Modification blocked: Advanced ProcCall detected."), confidential = TRUE)
return
var/new_flags = input_bitfield(
usr,
"Admin rights<br>This will affect only the current admin ([admin_ckey]), temporarily",
"admin_flags",
admin_holder.rank_flags(),
350,
590,
allowed_edit_field = usr.client.holder.can_edit_rights_flags(),
)
if(isnull(new_flags))
return
admin_holder.disassociate()
if (findtext(admin_holder.rank_names(), "([admin_ckey])"))
var/datum/admin_rank/rank = admin_holder.ranks[1]
rank.rights = new_flags
rank.include_rights = new_flags
rank.exclude_rights = NONE
rank.can_edit_rights = rank.can_edit_rights
else
// Not a modified subrank, need to duplicate the admin_rank datum to prevent modifying others too.
var/datum/admin_rank/new_admin_rank = new(
/* init_name = */ "[admin_holder.rank_names()]([admin_ckey])",
/* init_source = */ RANK_SOURCE_TEMPORARY,
/* init_rights = */ new_flags,
// rank_flags() includes the exclude rights, so we no longer need to handle them separately.
/* init_exclude_rights = */ NONE,
/* init_edit_rights = */ admin_holder.can_edit_rights_flags(),
)
admin_holder.ranks = list(new_admin_rank)
var/log = "[key_name(usr)] has updated the admin rights of [admin_ckey] into [rights2text(new_flags)]"
message_admins(log)
log_admin(log)
var/client/admin_client = GLOB.directory[admin_ckey]
admin_holder.associate(admin_client)
/// Polls usr for a new rank to add to either JUST this round, or the DB
/datum/admins/proc/add_rank()
if(!check_rights(R_PERMISSIONS))
to_chat(usr, span_adminprefix("You don't have the permissions for this."), confidential = TRUE)
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Rank Addition blocked: Advanced ProcCall detected."), confidential = TRUE)
return
if(usr.client.holder.can_edit_rights_flags() == NONE)
to_chat(usr, span_adminprefix("You are not allowed to add any rights."), confidential = TRUE)
return
var/new_rank_name = input("Please input a new rank", "New custom rank") as text|null
if (!new_rank_name)
return
var/list/datum/admin_rank/existing_ranks = ranks_from_rank_name(new_rank_name)
if (length(existing_ranks))
to_chat(usr, span_adminprefix("A rank by this name already exists, sorry!."), confidential = TRUE)
return
var/rights = input_bitfield(
usr,
"New rights for [new_rank_name]",
"admin_flags",
NONE,
350,
590,
allowed_edit_field = usr.client.holder.can_edit_rights_flags(),
)
var/excluded_rights = input_bitfield(
usr,
"New excluded rights for [new_rank_name]",
"admin_flags",
NONE,
350,
590,
allowed_edit_field = usr.client.holder.can_edit_rights_flags(),
)
var/edit_rights = input_bitfield(
usr,
"New editing rights for [new_rank_name]",
"admin_flags",
NONE,
350,
590,
allowed_edit_field = usr.client.holder.can_edit_rights_flags(),
)
var/use_db = FALSE
if(check_rights(R_DBRANKS, FALSE))
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Unable to connect to database, changes are temporary only."), confidential = TRUE)
use_db = FALSE
else
var/use_db_response = tgui_alert(usr,"Permanent changes are saved to the database for future rounds, temporary changes will affect only the current round", "Permanent or Temporary?", list("Permanent", "Temporary", "Cancel"))
if(isnull(use_db_response) || use_db_response == "Cancel")
return
if(use_db_response == "Permanent")
use_db = TRUE
else
use_db = FALSE
if(QDELETED(usr))
return
var/datum/admin_rank/custom_rank
if(use_db)
custom_rank = new(new_rank_name, RANK_SOURCE_DB, rights, excluded_rights, edit_rights)
else
custom_rank = new(new_rank_name, RANK_SOURCE_TEMPORARY, rights, excluded_rights, edit_rights)
if(QDELETED(custom_rank))
to_chat(usr, span_danger("Rank creation failed, check runtimes."), confidential = TRUE)
return
GLOB.admin_ranks += custom_rank
var/m1 = "[key_name_admin(usr)] created the new [use_db ? "permanent" : "temporary"] rank [new_rank_name]"
var/m2 = "[key_name(usr)] created the new [use_db ? "permanent" : "temporary"] rank [new_rank_name]"
if(!use_db)
message_admins(m1)
log_admin(m2)
return
// Shit check for conflicts
var/datum/db_query/query_rank_in_db = SSdbcore.NewQuery(
"SELECT 1 FROM [format_table_name("admin_ranks")] WHERE `rank` = :new_rank",
list("new_rank" = custom_rank.name)
)
if(!query_rank_in_db.warn_execute())
qdel(query_rank_in_db)
return
if(query_rank_in_db.NextRow())
qdel(query_rank_in_db)
return
QDEL_NULL(query_rank_in_db)
var/datum/db_query/query_add_rank = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_ranks")] (`rank`, flags, exclude_flags, can_edit_flags)
VALUES (:new_rank, :rights, :excluded_rights, :edit_rights)
"}, list("new_rank" = custom_rank.name, "rights" = rights, "excluded_rights" = excluded_rights, "edit_rights" = edit_rights))
if(!query_add_rank.warn_execute())
qdel(query_add_rank)
return
QDEL_NULL(query_add_rank)
message_admins(m1)
log_admin(m2)
var/datum/db_query/query_add_rank_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_RANK_ADDED]', :new_rank,
CONCAT('New rank added: ', :new_rank, ' (', :rights, ')', ' (', :excluded_rights, ')', ' (', :edit_rights, ')'))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "new_rank" = custom_rank.name,
"rights" = rights, "excluded_rights" = excluded_rights, "edit_rights" = edit_rights))
if(!query_add_rank_log.warn_execute())
qdel(query_add_rank_log)
return
QDEL_NULL(query_add_rank_log)
/// Removes a rank from the db/temp loading
/datum/admins/proc/remove_rank(admin_rank)
if(!admin_rank)
return
if(!check_rights(R_PERMISSIONS))
message_admins("[key_name_admin(usr)] attempted to remove a rank without sufficient rights.")
log_admin("[key_name(usr)] attempted to remove a rank without sufficient rights.")
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Rank Deletion blocked: Advanced ProcCall detected."), confidential = TRUE)
return
for(var/datum/admin_rank/R in GLOB.admin_ranks)
if(R.name == admin_rank && ((R.rights & usr.client.holder.can_edit_rights_flags()) != R.rights))
to_chat(usr, span_adminprefix("You don't have edit rights to all the rights this rank has, rank deletion not permitted."), confidential = TRUE)
return
var/list/datum/admin_rank/target_ranks = ranks_from_rank_name(admin_rank)
if (!target_ranks || length(target_ranks) > 1)
return
var/datum/admin_rank/target_rank = target_ranks[1]
var/local_only_deletion
switch(target_rank.source)
if(RANK_SOURCE_LOCAL)
to_chat(usr, span_adminprefix("Localhost rank cannot be deleted."), confidential = TRUE)
return
// This handles protected ranks on its own
if(RANK_SOURCE_TXT)
to_chat(usr, span_adminprefix("Text ranks cannot be meaningfully deleted, go modify admin_ranks.txt"), confidential = TRUE)
return
if(RANK_SOURCE_BACKUP)
to_chat(usr, span_adminprefix("Backup ranks cannot usefully be deleted, as they are stored in a temp json, go uh... edit that? I guess?."), confidential = TRUE)
return
if(RANK_SOURCE_TEMPORARY)
local_only_deletion = TRUE
if(RANK_SOURCE_DB)
local_only_deletion = FALSE
if(!local_only_deletion && CONFIG_GET(flag/load_legacy_ranks_only))
to_chat(usr, span_adminprefix("Database Rank deletion not permitted while database rank loading is disabled, deleting our local copy."), confidential = TRUE)
local_only_deletion = TRUE
if(!local_only_deletion)
var/datum/db_query/query_admins_with_rank = SSdbcore.NewQuery(
"SELECT 1 FROM [format_table_name("admin")] WHERE `rank` = :admin_rank",
list("admin_rank" = admin_rank)
)
if(!query_admins_with_rank.warn_execute())
qdel(query_admins_with_rank)
return
if(query_admins_with_rank.NextRow())
qdel(query_admins_with_rank)
to_chat(usr, span_danger("Error: Rank deletion attempted while db rank still used; Tell a coder, this shouldn't happen."), confidential = TRUE)
return
QDEL_NULL(query_admins_with_rank)
for(var/admin_name in GLOB.admin_datums)
var/datum/admins/existing_min = GLOB.admin_datums[admin_name]
if(target_rank in existing_min.ranks)
to_chat(usr, span_danger("Error: Rank deletion attempted while rank still used; Tell a coder, this shouldn't happen."), confidential = TRUE)
return
if(tgui_alert(usr,"Are you sure you want to remove [admin_rank]?", "Confirm Removal", list("Do it","Cancel")) != "Do it")
return
var/m1 = "[key_name_admin(usr)] removed rank [admin_rank] [local_only_deletion ? "temporarially" : "permanently"]"
var/m2 = "[key_name(usr)] removed rank [admin_rank] [local_only_deletion ? "temporarially" : "permanently"]"
GLOB.admin_ranks -= target_rank
QDEL_NULL(target_rank)
if(local_only_deletion)
message_admins(m1)
log_admin(m2)
return
var/datum/db_query/query_remove_rank = SSdbcore.NewQuery(
"DELETE FROM [format_table_name("admin_ranks")] WHERE `rank` = :admin_rank",
list("admin_rank" = admin_rank)
)
if(!query_remove_rank.warn_execute())
qdel(query_remove_rank)
return
QDEL_NULL(query_remove_rank)
message_admins(m1)
log_admin(m2)
var/datum/db_query/query_remove_rank_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_RANK_REMOVED]', :admin_rank, CONCAT('Rank removed: ', :admin_rank))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "admin_rank" = admin_rank))
if(!query_remove_rank_log.warn_execute())
qdel(query_remove_rank_log)
return
QDEL_NULL(query_remove_rank_log)
/// Changes the flags on either a DB or local rank
/datum/admins/proc/change_rank(admin_rank)
if(!admin_rank)
return
if(!check_rights(R_PERMISSIONS))
message_admins("[key_name_admin(usr)] attempted to edit rank permissions without sufficient rights.")
log_admin("[key_name(usr)] attempted to edit rank permissions without sufficient rights.")
return
if(IsAdminAdvancedProcCall())
to_chat(usr, span_adminprefix("Rank Edit blocked: Advanced ProcCall detected."), confidential = TRUE)
return
var/datum/asset/permissions_assets = get_asset_datum(/datum/asset/simple/namespaced/common)
permissions_assets.send(usr.client)
var/list/datum/admin_rank/target_ranks = ranks_from_rank_name(admin_rank)
if (!target_ranks || length(target_ranks) > 1)
return
var/datum/admin_rank/target_rank = target_ranks[1]
if(target_rank.name != admin_rank) // Somehow
to_chat(usr, span_adminprefix("Passed rank does not match target, somehow."), confidential = TRUE)
return
if((target_rank.rights & usr.client.holder.can_edit_rights_flags()) != target_rank.rights)
to_chat(usr, span_adminprefix("You don't have edit rights to all the rights this rank has, you aren't allowed to modify it."), confidential = TRUE)
return
var/attempt_db = FALSE
switch(target_rank.source)
if(RANK_SOURCE_LOCAL)
to_chat(usr, span_adminprefix("Localhost rank cannot be modified."), confidential = TRUE)
return
// This handles protected ranks on its own
if(RANK_SOURCE_TXT)
to_chat(usr, span_adminprefix("Text ranks cannot be meaningfully modified, go modify admin_ranks.txt"), confidential = TRUE)
return
if(RANK_SOURCE_BACKUP)
to_chat(usr, span_adminprefix("Backup ranks cannot usefully be modified, as they are stored in a temp json, go uh... edit that? I guess?."), confidential = TRUE)
return
// For completeness
if(RANK_SOURCE_TEMPORARY)
attempt_db = FALSE
if(RANK_SOURCE_DB)
if(!check_rights(R_DBRANKS, FALSE))
message_admins("[key_name_admin(usr)] attempted to edit db rank permissions without sufficient rights.")
log_admin("[key_name(usr)] attempted to edit db rank permissions without sufficient rights.")
return
attempt_db = TRUE
// We do not block editing ranks on protected admins which are not also protected
// This means an admin could in theory bypass protections if they modified a linked rank (such as game admin) which is not also protected
// It might be wise to make the permissions afforded by protected ranks inviolable. I'm unsure.
if(CONFIG_GET(flag/load_legacy_ranks_only))
to_chat(usr, span_adminprefix("Database rank loading is disabled, only temporary changes can be made to a rank's permissions."), confidential = TRUE)
attempt_db = FALSE
var/use_db = FALSE
if(attempt_db)
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Unable to connect to database, canceling."), confidential = TRUE)
return
use_db = TRUE
// Similarly, I want to shit check flags. This is sort of... paranoid but I want to be careful here
var/working_rights = NONE
var/working_exclude_rights = NONE
var/working_can_edit_rights = NONE
// Not allowed to permenantly edit a rank if it isn't IN the db already
// This is a real shitcheck but just to be sure
if(use_db)
var/datum/db_query/query_db_rank_info = SSdbcore.NewQuery({"
SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]
WHERE rank = :rank_name
"}, list("rank_name" = admin_rank))
if(!query_db_rank_info.warn_execute())
qdel(query_db_rank_info)
if(query_db_rank_info.NextRow())
working_rights = query_db_rank_info.item[1]
working_exclude_rights = query_db_rank_info.item[2]
working_can_edit_rights = query_db_rank_info.item[3]
else // Couldn't find anything, no db memes then
to_chat(usr, span_adminprefix("Rank does not exist in database, exiting."), confidential = TRUE)
qdel(query_db_rank_info)
return
QDEL_NULL(query_db_rank_info)
else
working_rights = target_rank.include_rights
working_exclude_rights = target_rank.exclude_rights
working_can_edit_rights = target_rank.can_edit_rights
while(TRUE)
var/what_to_edit = tgui_input_list(usr, "What do you want to edit", "Rank Editing", list("Rights", "Excluded Rights", "Edit Rights", "Finished"))
var/existing_flags = NONE
var/pretty_name
switch(what_to_edit)
if("Rights")
existing_flags = working_rights
pretty_name = "rights"
if("Excluded Rights")
existing_flags = working_exclude_rights
pretty_name = "excluded rights"
if("Edit Rights")
existing_flags = working_can_edit_rights
pretty_name = "editing rights"
else
return
var/new_flags = input_bitfield(
usr,
"Editing [target_rank.name] [what_to_edit]",
"admin_flags",
existing_flags,
350,
590,
allowed_edit_field = usr.client.holder.can_edit_rights_flags(),
)
if(isnull(new_flags))
to_chat(usr, span_adminprefix("Canceled editing rank."), confidential = TRUE)
return
// Gotta turn it off and on again
var/list/datum/admins/impacted_admins_to_client = list()
for(var/admin_key in GLOB.admin_datums)
var/datum/admins/checking = GLOB.admin_datums[admin_key]
if(!checking.owner)
continue
if(!(target_rank in checking.ranks))
continue
impacted_admins_to_client[checking] = checking.owner
checking.disassociate()
switch(what_to_edit)
if("Rights")
target_rank.include_rights = new_flags
working_rights = new_flags
if("Excluded Rights")
target_rank.exclude_rights = new_flags
working_exclude_rights = new_flags
if("Edit Rights")
target_rank.can_edit_rights = new_flags
working_can_edit_rights = new_flags
var/log = "[key_name(usr)] has [use_db ? "permenantly" : "temporarially"] updated the [pretty_name] of the [admin_rank] rank to [rights2text(new_flags)]"
message_admins(log)
log_admin(log)
for(var/datum/admins/modified as anything in impacted_admins_to_client)
modified.associate(impacted_admins_to_client[modified])
if(!use_db)
continue
// Only one at a time to avoid carrying over temp changes
// Doing it as we are does technically mean conflicts can occur, but that's rare enough I'm ok with it
var/datum/db_query/query_update_rank
switch(what_to_edit)
if("Rights")
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET flags = :flags
WHERE rank = :rank_name
"}, list("rank_name" = admin_rank, "flags" = new_flags))
if("Excluded Rights")
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET exclude_flags = :exclude_flags
WHERE rank = :rank_name
"}, list("rank_name" = admin_rank, "exclude_flags" = new_flags))
if("Editing Rights")
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET can_edit_flags = :can_edit_flags
WHERE rank = :rank_name
"}, list("rank_name" = admin_rank, "can_edit_flags" = new_flags))
if(!query_update_rank.warn_execute())
qdel(query_update_rank)
return
QDEL_NULL(query_update_rank)
var/datum/db_query/query_update_rank_log = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("admin_log")] (datetime, round_id, adminckey, adminip, operation, target, log)
VALUES (NOW(), :round_id, :adminckey, INET_ATON(:adminip), '[PERMISSIONS_ACTION_RANK_CHANGED]', :admin_rank, CONCAT('Rank changed: ', :admin_rank))
"}, list("round_id" = "[GLOB.round_id]", "adminckey" = usr.ckey, "adminip" = usr.client.address, "admin_rank" = admin_rank))
if(!query_update_rank_log.warn_execute())
qdel(query_update_rank_log)
return
QDEL_NULL(query_update_rank_log)
/datum/admins/proc/sync_lastadminrank(admin_ckey, admin_key, datum/admins/target_holder)
var/sqlrank = "Player"
if (target_holder)
sqlrank = target_holder.rank_names()
var/datum/db_query/query_sync_lastadminrank = SSdbcore.NewQuery(
"UPDATE [format_table_name("player")] SET lastadminrank = :rank WHERE ckey = :ckey",
list("rank" = sqlrank, "ckey" = admin_ckey)
)
if(!query_sync_lastadminrank.warn_execute())
qdel(query_sync_lastadminrank)
return
QDEL_NULL(query_sync_lastadminrank)
to_chat(usr, span_admin("Sync of [admin_key] successful."), confidential = TRUE)
#undef PERMISSIONS_LOGS_PER_PAGE