Files
Bubberstation/code/modules/admin/stickyban.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

486 lines
17 KiB
Plaintext

/datum/admins/proc/stickyban(action,data)
if(!check_rights(R_BAN))
return
switch (action)
if ("show")
stickyban_show()
if ("add")
var/list/ban = list()
var/ckey
ban["admin"] = usr.ckey
ban["type"] = list("sticky")
ban["reason"] = "(InGameBan)([usr.key])" //this will be displayed in dd only
if (data["ckey"])
ckey = ckey(data["ckey"])
else
ckey = input(usr,"Ckey","Ckey","") as text|null
if (!ckey)
return
ckey = ckey(ckey)
ban["ckey"] = ckey
if (get_stickyban_from_ckey(ckey))
to_chat(usr, span_adminnotice("Error: Can not add a stickyban: User already has a current sticky ban"), confidential = TRUE)
return
if (data["reason"])
ban["message"] = data["reason"]
else
var/reason = input(usr,"Reason","Reason","Ban Evasion") as text|null
if (!reason)
return
ban["message"] = "[reason]"
if(SSdbcore.Connect())
var/datum/db_query/query_create_stickyban = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("stickyban")] (ckey, reason, banning_admin)
VALUES (:ckey, :message, :banning_admin)
"}, list("ckey" = ckey, "message" = ban["message"], "banning_admin" = usr.ckey))
if (query_create_stickyban.warn_execute())
ban["fromdb"] = TRUE
qdel(query_create_stickyban)
world.SetConfig("ban",ckey,list2stickyban(ban))
ban = stickyban2list(list2stickyban(ban))
ban["matches_this_round"] = list()
ban["existing_user_matches_this_round"] = list()
ban["admin_matches_this_round"] = list()
ban["pending_matches_this_round"] = list()
SSstickyban.cache[ckey] = ban
log_admin_private("[key_name(usr)] has stickybanned [ckey].\nReason: [ban["message"]]")
message_admins(span_adminnotice("[key_name_admin(usr)] has stickybanned [ckey].\nReason: [ban["message"]]"))
if ("remove")
if (!data["ckey"])
return
var/ckey = data["ckey"]
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
if (tgui_alert(usr,"Are you sure you want to remove the sticky ban on [ckey]?","Are you sure",list("Yes","No")) == "No")
return
if (!get_stickyban_from_ckey(ckey))
to_chat(usr, span_adminnotice("Error: The ban disappeared."), confidential = TRUE)
return
world.SetConfig("ban",ckey, null)
SSstickyban.cache -= ckey
if (SSdbcore.Connect())
SSdbcore.QuerySelect(list(
SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban")] WHERE ckey = :ckey", list("ckey" = ckey)),
SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey", list("ckey" = ckey)),
SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_cid")] WHERE stickyban = :ckey", list("ckey" = ckey)),
SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ip")] WHERE stickyban = :ckey", list("ckey" = ckey))
), warn = TRUE, qdel = TRUE)
log_admin_private("[key_name(usr)] removed [ckey]'s stickyban")
message_admins(span_adminnotice("[key_name_admin(usr)] removed [ckey]'s stickyban"))
if ("remove_alt")
if (!data["ckey"])
return
var/ckey = data["ckey"]
if (!data["alt"])
return
var/alt = ckey(data["alt"])
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
var/key = LAZYACCESS(ban["keys"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt] is not linked to [ckey]'s sticky ban!"), confidential = TRUE)
return
if (tgui_alert(usr,"Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them, Use \[E] to exempt them","Are you sure",list("Yes","No")) == "No")
return
//we have to do this again incase something changes
ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: The ban disappeared."), confidential = TRUE)
return
key = LAZYACCESS(ban["keys"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt] link to [ckey]'s sticky ban disappeared."), confidential = TRUE)
return
LAZYREMOVE(ban["keys"], alt)
world.SetConfig("ban",ckey,list2stickyban(ban))
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
var/datum/db_query/query_remove_stickyban_alt = SSdbcore.NewQuery(
"DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey AND matched_ckey = :alt",
list("ckey" = ckey, "alt" = alt)
)
query_remove_stickyban_alt.warn_execute()
QDEL_NULL(query_remove_stickyban_alt)
log_admin_private("[key_name(usr)] has disassociated [alt] from [ckey]'s sticky ban")
message_admins(span_adminnotice("[key_name_admin(usr)] has disassociated [alt] from [ckey]'s sticky ban"))
if ("edit")
if (!data["ckey"])
return
var/ckey = data["ckey"]
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
var/oldreason = ban["message"]
var/reason = input(usr,"Reason","Reason","[ban["message"]]") as text|null
if (!reason || reason == oldreason)
return
//we have to do this again incase something changed while we waited for input
ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: The ban disappeared."), confidential = TRUE)
return
ban["message"] = "[reason]"
world.SetConfig("ban",ckey,list2stickyban(ban))
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
var/datum/db_query/query_edit_stickyban = SSdbcore.NewQuery(
"UPDATE [format_table_name("stickyban")] SET reason = :reason WHERE ckey = :ckey",
list("reason" = reason, "ckey" = ckey)
)
query_edit_stickyban.warn_execute()
QDEL_NULL(query_edit_stickyban)
log_admin_private("[key_name(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]")
message_admins(span_adminnotice("[key_name_admin(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]"))
if ("exempt")
if (!data["ckey"])
return
var/ckey = data["ckey"]
if (!data["alt"])
return
var/alt = ckey(data["alt"])
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
var/key = LAZYACCESS(ban["keys"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt] is not linked to [ckey]'s sticky ban!"), confidential = TRUE)
return
if (tgui_alert(usr,"Are you sure you want to exempt [alt] from [ckey]'s sticky ban?","Are you sure",list("Yes","No")) == "No")
return
//we have to do this again incase something changes
ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: The ban disappeared."), confidential = TRUE)
return
key = LAZYACCESS(ban["keys"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt]'s link to [ckey]'s sticky ban disappeared."), confidential = TRUE)
return
LAZYREMOVE(ban["keys"], alt)
key["exempt"] = TRUE
LAZYSET(ban["whitelist"], alt, key)
world.SetConfig("ban",ckey,list2stickyban(ban))
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
var/datum/db_query/query_exempt_stickyban_alt = SSdbcore.NewQuery(
"UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 1 WHERE stickyban = :ckey AND matched_ckey = :alt",
list("ckey" = ckey, "alt" = alt)
)
query_exempt_stickyban_alt.warn_execute()
QDEL_NULL(query_exempt_stickyban_alt)
log_admin_private("[key_name(usr)] has exempted [alt] from [ckey]'s sticky ban")
message_admins(span_adminnotice("[key_name_admin(usr)] has exempted [alt] from [ckey]'s sticky ban"))
if ("unexempt")
if (!data["ckey"])
return
var/ckey = data["ckey"]
if (!data["alt"])
return
var/alt = ckey(data["alt"])
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
var/key = LAZYACCESS(ban["whitelist"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt] is not exempt from [ckey]'s sticky ban!"), confidential = TRUE)
return
if (tgui_alert(usr,"Are you sure you want to unexempt [alt] from [ckey]'s sticky ban?","Are you sure",list("Yes","No")) == "No")
return
//we have to do this again incase something changes
ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: The ban disappeared."), confidential = TRUE)
return
key = LAZYACCESS(ban["whitelist"], alt)
if (!key)
to_chat(usr, span_adminnotice("Error: [alt]'s exemption from [ckey]'s sticky ban disappeared."), confidential = TRUE)
return
LAZYREMOVE(ban["whitelist"], alt)
key["exempt"] = FALSE
LAZYSET(ban["keys"], alt, key)
world.SetConfig("ban",ckey,list2stickyban(ban))
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
var/datum/db_query/query_unexempt_stickyban_alt = SSdbcore.NewQuery(
"UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 0 WHERE stickyban = :ckey AND matched_ckey = :alt",
list("ckey" = ckey, "alt" = alt)
)
query_unexempt_stickyban_alt.warn_execute()
QDEL_NULL(query_unexempt_stickyban_alt)
log_admin_private("[key_name(usr)] has unexempted [alt] from [ckey]'s sticky ban")
message_admins(span_adminnotice("[key_name_admin(usr)] has unexempted [alt] from [ckey]'s sticky ban"))
if ("timeout")
if (!data["ckey"])
return
if (!SSdbcore.Connect())
to_chat(usr, span_adminnotice("No database connection!"), confidential = TRUE)
return
var/ckey = data["ckey"]
if (tgui_alert(usr,"Are you sure you want to put [ckey]'s stickyban on timeout until next round (or removed)?","Are you sure",list("Yes","No")) == "No")
return
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
ban["timeout"] = TRUE
world.SetConfig("ban", ckey, null)
var/cachedban = SSstickyban.cache[ckey]
if (cachedban)
cachedban["timeout"] = TRUE
log_admin_private("[key_name(usr)] has put [ckey]'s sticky ban on timeout.")
message_admins(span_adminnotice("[key_name_admin(usr)] has put [ckey]'s sticky ban on timeout."))
if ("untimeout")
if (!data["ckey"])
return
if (!SSdbcore.Connect())
to_chat(usr, span_adminnotice("No database connection!"), confidential = TRUE)
return
var/ckey = data["ckey"]
if (tgui_alert(usr,"Are you sure you want to lift the timeout on [ckey]'s stickyban?","Are you sure",list("Yes","No")) == "No")
return
var/ban = get_stickyban_from_ckey(ckey)
var/cachedban = SSstickyban.cache[ckey]
if (cachedban)
cachedban["timeout"] = FALSE
if (!ban)
if (!cachedban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
ban = cachedban
ban["timeout"] = FALSE
world.SetConfig("ban",ckey,list2stickyban(ban))
log_admin_private("[key_name(usr)] has taken [ckey]'s sticky ban off of timeout.")
message_admins(span_adminnotice("[key_name_admin(usr)] has taken [ckey]'s sticky ban off of timeout."))
if ("revert")
if (!data["ckey"])
return
var/ckey = data["ckey"]
if (tgui_alert(usr,"Are you sure you want to revert the sticky ban on [ckey] to its state at round start (or last edit)?","Are you sure",list("Yes","No")) == "No")
return
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
to_chat(usr, span_adminnotice("Error: No sticky ban for [ckey] found!"), confidential = TRUE)
return
var/cached_ban = SSstickyban.cache[ckey]
if (!cached_ban)
to_chat(usr, span_adminnotice("Error: No cached sticky ban for [ckey] found!"), confidential = TRUE)
world.SetConfig("ban",ckey,null)
log_admin_private("[key_name(usr)] has reverted [ckey]'s sticky ban to its state at round start.")
message_admins(span_adminnotice("[key_name_admin(usr)] has reverted [ckey]'s sticky ban to its state at round start."))
//revert is mostly used when shit goes rouge, so we have to set it to null
// and wait a byond tick before assigning it to ensure byond clears its shit.
sleep(world.tick_lag)
world.SetConfig("ban",ckey,list2stickyban(cached_ban))
/datum/admins/proc/stickyban_gethtml(ckey)
var/ban = get_stickyban_from_ckey(ckey)
if (!ban)
return
var/timeout
if (SSdbcore.Connect())
timeout = "<a href='byond://?_src_=holder;[HrefToken()];stickyban=[(ban["timeout"] ? "untimeout" : "timeout")]&ckey=[ckey]'>\[[(ban["timeout"] ? "untimeout" : "timeout" )]\]</a>"
else
timeout = "<a href='byond://?_src_=holder;[HrefToken()];stickyban=revert&ckey=[ckey]'>\[revert\]</a>"
. = list({"
<a href='byond://?_src_=holder;[HrefToken()];stickyban=remove&ckey=[ckey]'>\[-\]</a>
[timeout]
<b>[ckey]</b>
<br />"
[ban["message"]] <b><a href='byond://?_src_=holder;[HrefToken()];stickyban=edit&ckey=[ckey]'>\[Edit\]</a></b><br />
"})
if (ban["admin"])
. += "[ban["admin"]]<br />"
else
. += "LEGACY<br />"
. += "Caught keys<br />\n<ol>"
for (var/key in ban["keys"])
if (ckey(key) == ckey)
continue
. += "<li><a href='byond://?_src_=holder;[HrefToken()];stickyban=remove_alt&ckey=[ckey]&alt=[ckey(key)]'>\[-\]</a>[key]<a href='byond://?_src_=holder;[HrefToken()];stickyban=exempt&ckey=[ckey]&alt=[ckey(key)]'>\[E\]</a></li>"
for (var/key in ban["whitelist"])
if (ckey(key) == ckey)
continue
. += "<li><a href='byond://?_src_=holder;[HrefToken()];stickyban=remove_alt&ckey=[ckey]&alt=[ckey(key)]'>\[-\]</a>[key]<a href='byond://?_src_=holder;[HrefToken()];stickyban=unexempt&ckey=[ckey]&alt=[ckey(key)]'>\[UE\]</a></li>"
. += "</ol>\n"
/datum/admins/proc/stickyban_show()
if(!check_rights(R_BAN))
return
var/list/bans = sticky_banned_ckeys()
var/list/banhtml = list()
for(var/key in bans)
var/ckey = ckey(key)
banhtml += "<br /><hr />\n"
banhtml += stickyban_gethtml(ckey)
var/html = {"
<head>
<title>Sticky Bans</title>
</head>
<body>
<h2>All Sticky Bans:</h2> <a href='byond://?_src_=holder;[HrefToken()];stickyban=add'>\[+\]</a><br>
[banhtml.Join("")]
</body>
"}
usr << browse(html,"window=stickybans;size=700x400")
/proc/sticky_banned_ckeys()
if (SSdbcore.Connect() || length(SSstickyban.dbcache))
if (SSstickyban.dbcacheexpire < world.time)
SSstickyban.Populatedbcache()
if (SSstickyban.dbcacheexpire)
return SSstickyban.dbcache.Copy()
return sort_list(world.GetConfig("ban"))
/proc/get_stickyban_from_ckey(ckey)
. = list()
if (!ckey)
return null
if (SSdbcore.Connect() || length(SSstickyban.dbcache))
if (SSstickyban.dbcacheexpire < world.time)
SSstickyban.Populatedbcache()
if (SSstickyban.dbcacheexpire)
. = SSstickyban.dbcache[ckey]
//reset the cache incase its a newer ban (but only if we didn't update the cache recently)
if (!. && SSstickyban.dbcacheexpire != world.time+STICKYBAN_DB_CACHE_TIME)
SSstickyban.dbcacheexpire = 1
SSstickyban.Populatedbcache()
. = SSstickyban.dbcache[ckey]
if (.)
var/list/cachedban = SSstickyban.cache["[ckey]"]
if (cachedban)
.["timeout"] = cachedban["timeout"]
.["fromdb"] = TRUE
return
. = stickyban2list(world.GetConfig("ban", ckey)) || stickyban2list(world.GetConfig("ban", ckey(ckey))) || list()
if (!length(.))
return null
/proc/stickyban2list(ban, strictdb = TRUE)
if (!ban)
return null
. = params2list(ban)
if (.["keys"])
var/keys = splittext(.["keys"], ",")
var/ckeys = list()
for (var/key in keys)
var/ckey = ckey(key)
ckeys[ckey] = ckey //to make searching faster.
.["keys"] = ckeys
if (.["whitelist"])
var/keys = splittext(.["whitelist"], ",")
var/ckeys = list()
for (var/key in keys)
var/ckey = ckey(key)
ckeys[ckey] = ckey //to make searching faster.
.["whitelist"] = ckeys
.["type"] = splittext(.["type"], ",")
.["IP"] = splittext(.["IP"], ",")
.["computer_id"] = splittext(.["computer_id"], ",")
. -= "fromdb"
/proc/list2stickyban(list/ban)
if (!ban || !islist(ban))
return null
. = ban.Copy()
if (.["keys"])
.["keys"] = jointext(.["keys"], ",")
if (.["IP"])
.["IP"] = jointext(.["IP"], ",")
if (.["computer_id"])
.["computer_id"] = jointext(.["computer_id"], ",")
if (.["whitelist"])
.["whitelist"] = jointext(.["whitelist"], ",")
if (.["type"])
.["type"] = jointext(.["type"], ",")
. -= "reverting"
. -= "matches_this_round"
. -= "existing_user_matches_this_round"
. -= "admin_matches_this_round"
. -= "pending_matches_this_round"
. = list2params(.)
ADMIN_VERB(panel_sticky_ban, R_BAN, "Sticky Ban Panel", "List and manage sticky bans.", ADMIN_CATEGORY_MAIN)
user.holder.stickyban_show()