Implements a path-based, player-specific whitelisting system (#8725)

* Implements a path-based, player-specific whitelisting system

* Readds ported functions

* Local testing

* Copy-paste error

* VV modifications, player panel interface

* Testing admin operations

* data review
This commit is contained in:
Atermonera
2022-10-22 11:34:19 -08:00
committed by GitHub
parent 4ffb64530a
commit 712c59328f
14 changed files with 377 additions and 91 deletions

View File

@@ -55,3 +55,7 @@
// /atom
#define VV_HK_ATOM_EXPLODE "turf_explode"
#define VV_HK_ATOM_EMP "turf_emp"
// /client
#define VV_HK_ADD_WHITELIST "add_whitelist"
#define VV_HK_DEL_WHITELIST "del_whitelist"

View File

@@ -58,3 +58,54 @@
fileaccess_timer = world.time + FTPDELAY
return 0
#undef FTPDELAY
/// Reads path as a text file, splitting it on delimiter matches.
/proc/read_lines(path)
var/static/regex/pattern = regex(@"\r?\n")
return splittext_char(file2text(path) || "", pattern)
/// Read path as a text file to a list, stripping empty space and comments.
/proc/read_commentable(path)
var/static/regex/pattern = regex(@"^([^#]+)")
to_world_log("PATTERN: [pattern] [istype(pattern)]")
var/list/result = list()
for (var/line in read_lines(path))
if (!pattern.Find_char(line))
continue
line = trim(pattern.group[1])
if (!line)
continue
result += line
return result
/// Read path as a text file to a map of key value or key list pairs.
/proc/read_config(path, lowercase_keys = TRUE)
var/static/regex/pattern = regex(@"\s+")
var/list/result = list()
for (var/line in read_commentable(path))
if (!pattern.Find_char(line))
if (lowercase_keys)
line = lowertext(line)
if (!result[line])
result[line] = TRUE
else if (result[line] != TRUE)
log_error({"Mixed-type key "[line]" discovered in config file "[path]"!"})
else
log_debug({"Duplicate key "[line]" discovered in config file "[path]"!"})
continue
var/key = copytext_char(line, 1, pattern.index)
if (lowercase_keys)
key = lowertext(key)
var/value = copytext_char(line, pattern.index + 1)
if (!result[key])
result[key] = value
continue
if (!islist(result[key]))
if (result[key] == TRUE)
log_error({"Mixed-type key "[key]" discovered in config file "[path]"!"})
continue
result[key] = list(result[key])
result[key] += value
return result

View File

@@ -1,85 +0,0 @@
var/global/list/whitelist = list()
/hook/startup/proc/loadWhitelist()
if(config.usewhitelist)
load_whitelist()
return TRUE
/proc/load_whitelist()
whitelist = file2list("data/whitelist.txt")
if(!whitelist.len) whitelist = null
/proc/check_whitelist(mob/M /*, var/rank*/)
if(!whitelist)
return FALSE
return ("[M.ckey]" in whitelist)
var/global/list/alien_whitelist = list()
/hook/startup/proc/loadAlienWhitelist()
if(config.usealienwhitelist)
load_alienwhitelist()
return TRUE
/proc/load_alienwhitelist()
var/text = file2text("config/alienwhitelist.txt")
if (!text)
log_misc("Failed to load config/alienwhitelist.txt")
else
alien_whitelist = splittext(text, "\n")
/proc/is_alien_whitelisted(mob/M, var/datum/species/species)
//They are admin or the whitelist isn't in use
if(whitelist_overrides(M))
return TRUE
//You did something wrong
if(!M || !species)
return FALSE
//The species isn't even whitelisted
if(!(species.spawn_flags & SPECIES_IS_WHITELISTED))
return TRUE
//If we have a loaded file, search it
if(alien_whitelist)
for (var/s in alien_whitelist)
if(findtext(s,"[M.ckey] - [species.name]"))
return TRUE
if(findtext(s,"[M.ckey] - All"))
return TRUE
/proc/is_lang_whitelisted(mob/M, var/datum/language/language)
//They are admin or the whitelist isn't in use
if(whitelist_overrides(M))
return TRUE
//You did something wrong
if(!M || !language)
return FALSE
//The language isn't even whitelisted
if(!(language.flags & WHITELISTED))
return TRUE
//If we have a loaded file, search it
if(alien_whitelist)
for (var/s in alien_whitelist)
if(findtext(s,"[M.ckey] - [language.name]"))
return TRUE
if(findtext(s,"[M.ckey] - All"))
return TRUE
/proc/whitelist_overrides(mob/M)
return !config.usealienwhitelist || check_rights(R_ADMIN|R_EVENT, 0, M)
var/global/list/genemod_whitelist = list()
/hook/startup/proc/LoadGenemodWhitelist()
global.genemod_whitelist = file2list("config/genemodwhitelist.txt")
return TRUE
/proc/is_genemod_whitelisted(mob/M)
return M && M.client && M.client.ckey && LAZYLEN(global.genemod_whitelist) && (M.client.ckey in global.genemod_whitelist)
/proc/foo()
to_world(list2text(global.genemod_whitelist))

View File

@@ -97,6 +97,11 @@ var/global/floorIsLava = 0
"}
if (M.client)
body += "<br><br><b>Whitelists:</b><br>"
body += jointext(M.client.get_whitelists_list(), ", ")
body += "<br><A href='?src=\ref[src];modify_whitelist=\ref[M.client];set_value=1'>Add Whitelist</A>"
body += " - <A href='?src=\ref[src];modify_whitelist=\ref[M.client];set_value=0'>Remove Whitelist</A>"
if(!istype(M, /mob/new_player))
body += "<br><br>"
body += "<b>Transformation:</b>"

View File

@@ -107,7 +107,9 @@ var/global/list/admin_verbs_admin = list(
/datum/admins/proc/sendFax,
/client/proc/despawn_player,
/datum/admins/proc/view_feedback,
/client/proc/debug_global_variables
/client/proc/debug_global_variables,
/client/proc/admin_add_whitelist,
/client/proc/admin_del_whitelist
)
var/global/list/admin_verbs_ban = list(
@@ -236,7 +238,9 @@ var/global/list/admin_verbs_debug = list(
/datum/admins/proc/view_feedback,
/client/proc/debug_global_variables,
/client/proc/ping_webhook,
/client/proc/reload_webhooks
/client/proc/reload_webhooks,
/client/proc/admin_add_whitelist,
/client/proc/admin_del_whitelist
)
var/global/list/admin_verbs_paranoid_debug = list(

View File

@@ -23,7 +23,7 @@ var/global/jobban_keylist[0] //to store the keys & ranks
if (guest_jobbans(rank))
if(config.guest_jobban && IsGuestKey(M.key))
return "Guest Job-ban"
if(config.usewhitelist && !check_whitelist(M))
if(config.usewhitelist && !M.client.is_whitelisted(rank)) // This outright doesn't work, but at least compiles. AFAIK we don't use this system at present. ~Ater
return "Whitelisted Job"
return ckey_is_jobbanned(M.ckey, rank)

View File

@@ -1940,6 +1940,18 @@
PlayerNotesFilter()
return
if(href_list["modify_whitelist"])
var/client/C = locate(href_list["modify_whitelist"])
if(!istype(C))
return
var/entry = input(usr, "Please enter the path of the whitelist you wish to modify:", "Whitelist target", "") as text|null
if(!entry || !ispath(text2path(entry)))
return
if(href_list["set_value"] == "1")
C.add_whitelist(entry)
else if(href_list["set_value"] == "0")
C.remove_whitelist(entry)
/mob/living/proc/can_centcom_reply()
return 0

View File

@@ -76,7 +76,6 @@ GLOBAL_DATUM(debug_real_globals, /debug_real_globals) // :3c
"admin_log",
"admin_ranks",
"admin_state",
"alien_whitelist",
"alldirs",
"ahelp_tickets",
"adminfaxes",

View File

@@ -77,7 +77,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
// That last list is entirely arbitrary. Take complaints up with Kholdstare.
if((istype(client) && check_rights(R_ADMIN | R_EVENT | R_FUN, 0, client)) || \
(LAZYLEN(instance.species_allowed) && species && (species in instance.species_allowed)) || \
(config.genemod_whitelist && is_genemod_whitelisted(src) && LAZYLEN(instance.whitelist_allowed) && (species in instance.whitelist_allowed)))
(config.genemod_whitelist && client.is_whitelisted(/whitelist/genemod) && LAZYLEN(instance.whitelist_allowed) && (species in instance.whitelist_allowed)))
.[instance.name] = instance
/datum/category_item/player_setup_item/general/body

View File

@@ -0,0 +1,125 @@
// Rewrites the client's whitelists to disk
/proc/write_whitelist(var/key, var/list/whitelist)
var/filename = "data/player_saves/[copytext(ckey(key),1,2)]/[ckey(key)]/whitelist.json"
log_admin("Writing whitelists to disk for [key] at `[filename]`")
try
// Byond doesn't have a mechanism for in-place modification of a file, so we have to make a new one and then overwrite the old one.
// If this is interrupted, the .tmp file exists and can be loaded at the start of the next round.
// The in-game list represents the set of valid entries within the whitelist file, so we may as well remove invalid lines in the process.
text2file(json_encode(whitelist), filename + ".tmp")
if(fexists(filename) && !fdel(filename))
error("Exception when overwriting whitelist file [filename]")
if(fcopy(filename + ".tmp", filename))
if(!fdel(filename + ".tmp"))
error("Exception when deleting tmp whitelist file [filename].tmp")
catch(var/exception/E)
error("Exception when writing to whitelist file [filename]: [E]")
// Add the selected path to the player's whitelists, if it's valid.
/client/proc/add_whitelist(var/path)
if(istext(path))
path = text2path(path)
if(!ispath(path))
return
// If they're already whitelisted, do nothing (Also loads the whitelist)
if(is_whitelisted(path))
return
log_and_message_admins("[usr ? usr : "SYSTEM"] giving [path] whitelist to [src]", usr)
src.whitelists[path] = TRUE
write_whitelist(src.ckey, src.whitelists)
// Remove the selected path from the player's whitelists.
/client/proc/remove_whitelist(var/path)
if(istext(path))
path = text2path(path)
if(!ispath(path))
return
// If they're not whitelisted, do nothing (Also loads the whitelist)
if(!is_whitelisted(path))
return
log_and_message_admins("[usr ? usr : "SYSTEM"] removing [path] whitelist from [src]", usr)
src.whitelists -= path
write_whitelist(src.ckey, src.whitelists)
/client/proc/admin_add_whitelist()
set name = "Whitelist Add Player"
set category = "Admin"
set desc = "Give a whitelist to a target player"
admin_modify_whitelist(TRUE)
/client/proc/admin_del_whitelist()
set name = "Whitelist Remove Player"
set category = "Admin"
set desc = "Remove a whitelist from the target player"
admin_modify_whitelist(FALSE)
/client/proc/admin_modify_whitelist(var/set_value)
if(!check_rights(R_ADMIN|R_DEBUG))
return
// Get the person to whitelist.
var/key = input(src, "Please enter the CKEY of the player whose whitelist you wish to modify:", "Whitelist ckey", "") as text|null
if(!key || !length(key))
return
key = ckey(key)
if(!fexists("data/player_saves/[copytext(key,1,2)]/[key]/preferences.sav"))
to_chat(src, "That player doesn't seem to exist...")
return
// Get the whitelist thing to modify.
var/entry = input(src, "Please enter the path of the whitelist you wish to modify:", "Whitelist target", "") as text|null
if(!entry || !ispath(text2path(entry)))
return
// If they're logged in, modify it directly.
var/client/C = ckey2client(key)
if(istype(C))
set_value ? C.add_whitelist(entry) : C.remove_whitelist(entry)
return
log_and_message_admins("[src] [set_value ? "giving [entry] whitelist to" : "removing [entry] whitelist from"] [key]", src)
// Else, we have to find and modify the whitelist file ourselves.
var/list/whitelists = load_whitelist(key)
// They're already whitelisted.
if(whitelists[entry] == set_value)
return
whitelists[entry] = set_value
write_whitelist(key, whitelists)
/client/vv_get_dropdown()
. = ..()
VV_DROPDOWN_OPTION(VV_HK_ADD_WHITELIST, "Add whitelist")
VV_DROPDOWN_OPTION(VV_HK_DEL_WHITELIST, "Remove whitelist")
/client/vv_do_topic(list/href_list)
. = ..()
IF_VV_OPTION(VV_HK_ADD_WHITELIST)
if(!check_rights(R_ADMIN|R_DEBUG))
return
var/entry = input(usr, "Please enter the path of the whitelist you wish to add:", "Whitelist target", "") as text|null
if(!entry || !ispath(text2path(entry)))
return
var/client/C = locate(href_list["target"])
if(istype(C))
C.add_whitelist(entry)
IF_VV_OPTION(VV_HK_DEL_WHITELIST)
if(!check_rights(R_ADMIN|R_DEBUG))
return
var/entry = input(usr, "Please enter the path of the whitelist you wish to remove:", "Whitelist target", "") as text|null
if(!entry || !ispath(text2path(entry)))
return
var/client/C = locate(href_list["target"])
if(istype(C))
C.remove_whitelist(entry)

View File

@@ -0,0 +1,65 @@
// Proc to convert the old whitelist systems into the new, shiny, modern system.
// This has to do a lot of file I/O because it handles _everyone's_ whitelists, so it's expected to be pretty expensive.
// Hence, this has to be manually called by an admin.
/proc/load_legacy_whitelist()
var/static/list/alienwhitelist_dict = list(
"Alai" = /datum/language/tajsign,
"Common Skrellian" = /datum/language/skrell,
"Diona" = /datum/species/diona,
"Promethean" = /datum/species/shapeshifter/promethean,
"Schechi" = /datum/language/teshari,
"Siik" = /datum/language/tajaran,
"Sinta'Unathi" = /datum/language/unathi,
"Skrell" = /datum/species/skrell,
"Sol Common" = /datum/language/human,
"Tajara" = /datum/species/tajaran,
"Teshari" = /datum/species/teshari,
"Unathi" = /datum/species/unathi,
"Zaddat" = /datum/species/zaddat,
)
var/list/whitelists_to_write = list()
// Load in the alien whitelists
for(var/line in read_lines("config/alienwhitelist.txt"))
if(!length(line))
continue
var/static/regex/R = regex(" - ")
var/list/tok = splittext(line, R)
// Whitelist is no longer valid.
if(length(tok) < 2 || !(tok[2] in alienwhitelist_dict))
continue
var/key = ckey(tok[1])
// If they don't have a preferences save file, then they probably don't play here any more.
if(!fexists("data/player_saves/[copytext(key,1,2)]/[key]/preferences.sav"))
continue
LAZYADDASSOC(whitelists_to_write[key], alienwhitelist_dict[tok[2]], TRUE)
// Load in the genemod whitelist
for(var/line in read_commentable("config/genemodwhitelist.txt"))
var/key = ckey(line)
// If they don't have a preferences save file, then they probably don't play here any more.
if(!fexists("data/player_saves/[copytext(key,1,2)]/[key]/preferences.sav"))
continue
LAZYADDASSOC(whitelists_to_write[key], /whitelist/genemod, TRUE)
// Write the whitelists to files. Second stage, so we don't make a million individual writes.
for(var/key in whitelists_to_write)
var/filename = "data/player_saves/[copytext(key,1,2)]/[key]/whitelist.json"
var/list/whitelist = whitelists_to_write[key]
try
// If the file already exists, don't just blindly overwrite it.
if(fexists(filename))
var/prior_whitelist = file2text(filename) || ""
if(length(prior_whitelist))
whitelist |= json_decode(prior_whitelist)
text2file(json_encode(whitelist), filename + ".tmp")
if(fexists(filename) && !fdel(filename))
error("Error overwriting whitelist file [filename]")
if(!fcopy(filename + ".tmp", filename) || !fdel(filename + ".tmp"))
error("Exception when deleting tmp whitelist file [filename].tmp")
catch(var/exception/E)
error("Exception when writing to whitelist file [filename]: [E]")

View File

@@ -0,0 +1,4 @@
// This file is reserved for creating paths that can be used to whitelist
// things that aren't directly attributable to specific paths.
/whitelist/genemod // This whitelists a collection of sprite_accessories
var/name = "Genemods"

View File

@@ -0,0 +1,99 @@
/client/var/list/whitelists = null
// Prints the client's whitelist entries
/client/verb/print_whitelist()
set name = "Show Whitelist Entries"
set desc = "Print the set of things you're whitelisted for."
set category = "OOC"
to_chat(src, "You are whitelisted for:")
to_chat(src, jointext(get_whitelists_list(), "\n"))
/client/proc/get_whitelists_list()
. = list()
if(src.whitelists == null)
src.whitelists = load_whitelist(src.ckey)
for(var/key in src.whitelists)
try
. += initial(initial(key:name))
catch()
. += key
/proc/load_whitelist(var/key)
var/filename = "data/player_saves/[copytext(ckey(key),1,2)]/[ckey(key)]/whitelist.json"
try
// Check the player-specific whitelist file, if it exists.
if(fexists(filename))
// Load the whitelist entries from file, or empty string if empty.`
. = list()
for(var/T in json_decode(file2text(filename) || ""))
T = text2path(T)
if(!ispath(T))
continue
.[T] = TRUE
// Something was removing an entry from the whitelist and interrupted mid-overwrite.
else if(fexists(filename + ".tmp") && fcopy(filename + ".tmp", filename))
. = load_whitelist(key)
if(!fdel(filename + ".tmp"))
error("Exception when deleting tmp whitelist file [filename].tmp")
// Whitelist file doesn't exist, so they aren't whitelisted for anything. Create the file.
else if(fexists("data/player_saves/[copytext(ckey(key),1,2)]/[ckey(key)]/preferences.sav"))
text2file("", filename)
. = list()
catch(var/exception/E)
error("Exception when loading whitelist file [filename]: [E]")
// Returns true if the specified path is in the player's whitelists, false otw.
/client/proc/is_whitelisted(var/path)
if(istext(path))
path = text2path(path)
if(!ispath(path))
return
// If it hasn't already been loaded, load it.
if(src.whitelists == null)
src.whitelists = load_whitelist(src.ckey)
return src.whitelists[path]
/proc/is_alien_whitelisted(mob/M, var/datum/species/species)
//They are admin or the whitelist isn't in use
if(whitelist_overrides(M))
return TRUE
//You did something wrong
if(!M || !species)
return FALSE
//The species isn't even whitelisted
if(!(species.spawn_flags & SPECIES_IS_WHITELISTED))
return TRUE
var/client/C = (!isclient(M)) ? M.client : M
return C.is_whitelisted(species.type)
/proc/is_lang_whitelisted(mob/M, var/datum/language/language)
//They are admin or the whitelist isn't in use
if(whitelist_overrides(M))
return TRUE
//You did something wrong
if(!M || !language || !M.client)
return FALSE
//The language isn't even whitelisted
if(!(language.flags & WHITELISTED))
return TRUE
var/client/C = (!isclient(M)) ? M.client : M
return C.is_whitelisted(language.type)
/proc/whitelist_overrides(mob/M)
return !config.usealienwhitelist || check_rights(R_ADMIN|R_EVENT, 0, M)

View File

@@ -655,7 +655,6 @@
#include "code\game\jobs\access_datum.dm"
#include "code\game\jobs\job_controller.dm"
#include "code\game\jobs\jobs.dm"
#include "code\game\jobs\whitelist.dm"
#include "code\game\jobs\job\_alt_title.dm"
#include "code\game\jobs\job\assistant.dm"
#include "code\game\jobs\job\captain.dm"
@@ -3196,6 +3195,10 @@
#include "code\modules\webhooks\webhook_roundend.dm"
#include "code\modules\webhooks\webhook_roundprep.dm"
#include "code\modules\webhooks\webhook_roundstart.dm"
#include "code\modules\whitelist\admin_ops.dm"
#include "code\modules\whitelist\legacy.dm"
#include "code\modules\whitelist\phony_paths.dm"
#include "code\modules\whitelist\whitelist.dm"
#include "code\modules\xenoarcheaology\anomaly_container.dm"
#include "code\modules\xenoarcheaology\boulder.dm"
#include "code\modules\xenoarcheaology\effect.dm"