mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-14 09:35:30 +01:00
[MIRROR] 2FA for Admins (#6221)
* 2FA for Admins * a Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Co-authored-by: Gandalf <jzo123@hotmail.com>
This commit is contained in:
@@ -818,3 +818,10 @@ GLOBAL_PROTECT(admin_verbs_hideable)
|
||||
set category = "Debug"
|
||||
|
||||
src << output("", "statbrowser:create_debug")
|
||||
|
||||
/client/proc/admin_2fa_verify()
|
||||
set name = "Verify Admin"
|
||||
set category = "Admin"
|
||||
|
||||
var/datum/admins/admin = GLOB.admin_datums[ckey]
|
||||
admin?.associate(src)
|
||||
|
||||
+193
-15
@@ -6,6 +6,9 @@ GLOBAL_PROTECT(protected_admins)
|
||||
GLOBAL_VAR_INIT(href_token, GenerateToken())
|
||||
GLOBAL_PROTECT(href_token)
|
||||
|
||||
#define RESULT_2FA_VALID 1
|
||||
#define RESULT_2FA_ID 2
|
||||
|
||||
/datum/admins
|
||||
var/datum/admin_rank/rank
|
||||
|
||||
@@ -30,6 +33,12 @@ GLOBAL_PROTECT(href_token)
|
||||
|
||||
var/datum/filter_editor/filteriffic
|
||||
|
||||
/// Whether or not the user tried to connect, but was blocked by 2FA
|
||||
var/blocked_by_2fa = FALSE
|
||||
|
||||
/// Whether or not this user can bypass 2FA
|
||||
var/bypass_2fa = FALSE
|
||||
|
||||
/datum/admins/New(datum/admin_rank/R, ckey, force_active = FALSE, protected)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
@@ -95,27 +104,45 @@ GLOBAL_PROTECT(href_token)
|
||||
disassociate()
|
||||
add_verb(C, /client/proc/readmin)
|
||||
|
||||
/datum/admins/proc/associate(client/C)
|
||||
/datum/admins/proc/associate(client/client)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
|
||||
if(istype(C))
|
||||
if(C.ckey != target)
|
||||
var/msg = " has attempted to associate with [target]'s admin datum"
|
||||
message_admins("[key_name_admin(C)][msg]")
|
||||
log_admin("[key_name(C)][msg]")
|
||||
return
|
||||
if (deadmined)
|
||||
activate()
|
||||
owner = C
|
||||
owner.holder = src
|
||||
owner.add_admin_verbs() //TODO <--- todo what? the proc clearly exists and works since its the backbone to our entire admin system
|
||||
remove_verb(owner, /client/proc/readmin)
|
||||
owner.init_verbs() //re-initialize the verb list
|
||||
GLOB.admins |= C
|
||||
if(!istype(client))
|
||||
return
|
||||
|
||||
if(client?.ckey != target)
|
||||
var/msg = " has attempted to associate with [target]'s admin datum"
|
||||
message_admins("[key_name_admin(client)][msg]")
|
||||
log_admin("[key_name(client)][msg]")
|
||||
return
|
||||
|
||||
var/result_2fa = check_2fa(client)
|
||||
if (!result_2fa[RESULT_2FA_VALID])
|
||||
blocked_by_2fa = TRUE
|
||||
alert_2fa_necessary(client)
|
||||
start_2fa_process(client, result_2fa[RESULT_2FA_ID])
|
||||
|
||||
return
|
||||
else if (blocked_by_2fa)
|
||||
sync_lastadminrank(client.ckey, client.key)
|
||||
|
||||
blocked_by_2fa = FALSE
|
||||
|
||||
if (deadmined)
|
||||
activate()
|
||||
|
||||
remove_verb(client, /client/proc/admin_2fa_verify)
|
||||
|
||||
owner = client
|
||||
owner.holder = src
|
||||
owner.add_admin_verbs()
|
||||
remove_verb(owner, /client/proc/readmin)
|
||||
owner.init_verbs() //re-initialize the verb list
|
||||
GLOB.admins |= client
|
||||
|
||||
/datum/admins/proc/disassociate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
@@ -148,6 +175,154 @@ GLOBAL_PROTECT(href_token)
|
||||
return TRUE //we have all the rights they have and more
|
||||
return FALSE
|
||||
|
||||
// TRUE for a vaild connection, null is the id (it is unnecessary)
|
||||
#define VALID_2FA_CONNECTION list(TRUE, null)
|
||||
|
||||
/// Returns whether or not the given client has a verified 2FA connection.
|
||||
/// The output is in the form of a list with the first index being whether or not the
|
||||
/// check was successful, the 2nd is the ID of the associated database entry
|
||||
/// if its a false result and if one can be found.
|
||||
/datum/admins/proc/check_2fa(client/client)
|
||||
if (bypass_2fa)
|
||||
return VALID_2FA_CONNECTION
|
||||
|
||||
var/admin_2fa_url = CONFIG_GET(string/admin_2fa_url)
|
||||
|
||||
// 2FA not being enabled == everyone passes
|
||||
if (isnull(admin_2fa_url) || admin_2fa_url == "")
|
||||
return VALID_2FA_CONNECTION
|
||||
|
||||
// I believe this is only in the case of Dream Seeker.
|
||||
if (isnull(client?.address))
|
||||
return VALID_2FA_CONNECTION
|
||||
|
||||
if (!SSdbcore.Connect())
|
||||
if (verify_backup_data(client))
|
||||
return VALID_2FA_CONNECTION
|
||||
else
|
||||
return list(FALSE, null)
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery({"
|
||||
SELECT id, verification_time FROM [format_table_name("admin_connections")]
|
||||
WHERE ckey = :ckey
|
||||
AND ip = INET_ATON(:ip)
|
||||
AND cid = :cid
|
||||
"}, list(
|
||||
"ckey" = client.ckey,
|
||||
"ip" = client.address,
|
||||
"cid" = client.computer_id,
|
||||
))
|
||||
|
||||
if (!query.Execute())
|
||||
qdel(query)
|
||||
return list(FALSE, null)
|
||||
|
||||
var/is_valid = FALSE
|
||||
var/id = null
|
||||
|
||||
if (query.NextRow())
|
||||
id = query.item[1]
|
||||
is_valid = !isnull(query.item[2])
|
||||
|
||||
qdel(query)
|
||||
return list(is_valid, id)
|
||||
|
||||
#undef VALID_2FA_CONNECTION
|
||||
|
||||
#define ERROR_2FA_REQUEST_PERMISSIONS "<h1><b class='danger'>You could not be verified, and a DB connection couldn't be established. Please contact an admin with +PERMISSIONS to grant you permission.</b></h1>"
|
||||
|
||||
/datum/admins/proc/start_2fa_process(client/client, id)
|
||||
add_verb(client, /client/proc/admin_2fa_verify)
|
||||
client?.init_verbs()
|
||||
|
||||
var/admin_2fa_url = CONFIG_GET(string/admin_2fa_url)
|
||||
|
||||
if (!SSdbcore.Connect())
|
||||
to_chat(
|
||||
client,
|
||||
type = MESSAGE_TYPE_ADMINLOG,
|
||||
html = ERROR_2FA_REQUEST_PERMISSIONS,
|
||||
confidential = TRUE,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if (isnull(id))
|
||||
var/datum/db_query/insert_query = SSdbcore.NewQuery({"
|
||||
INSERT INTO [format_table_name("admin_connections")] (ckey, ip, cid)
|
||||
VALUES(:ckey, INET_ATON(:ip), :cid)
|
||||
"}, list(
|
||||
"ckey" = client.ckey,
|
||||
"ip" = client.address,
|
||||
"cid" = client.computer_id,
|
||||
))
|
||||
|
||||
if (!insert_query.Execute())
|
||||
qdel(insert_query)
|
||||
to_chat(
|
||||
client,
|
||||
type = MESSAGE_TYPE_ADMINLOG,
|
||||
html = ERROR_2FA_REQUEST_PERMISSIONS,
|
||||
confidential = TRUE,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
id = insert_query.last_insert_id
|
||||
|
||||
var/url_for_2fa = replacetextEx(admin_2fa_url, "%ID%", id)
|
||||
to_chat(
|
||||
client,
|
||||
type = MESSAGE_TYPE_ADMINLOG,
|
||||
html = {"
|
||||
<h1><b class='danger'>You could not be verified.</b></h1>
|
||||
<h2><b class='danger'>Please visit <a href='[url_for_2fa]'>[url_for_2fa]</a> to verify.</b></h2>
|
||||
<h2><b class='danger'>When you are done, click the 'Verify Admin' button in your admin tab.</b></h2>
|
||||
"},
|
||||
confidential = TRUE,
|
||||
)
|
||||
|
||||
#undef ERROR_2FA_REQUEST_PERMISSIONS
|
||||
|
||||
/datum/admins/proc/verify_backup_data(client/client)
|
||||
var/backup_file = file2text("data/admins_backup.json")
|
||||
if (isnull(backup_file))
|
||||
log_world("Unable to locate admins backup file.")
|
||||
return FALSE
|
||||
|
||||
var/list/backup_file_json = json_decode(backup_file)
|
||||
var/connections = backup_file_json["connections"]
|
||||
|
||||
// This can happen for older admins_backup.json files
|
||||
if (isnull(connections))
|
||||
return FALSE
|
||||
|
||||
var/most_recent_valid_connection = connections[client?.ckey]
|
||||
if (isnull(most_recent_valid_connection))
|
||||
return FALSE
|
||||
|
||||
return most_recent_valid_connection["cid"] == client?.computer_id \
|
||||
&& most_recent_valid_connection["ip"] == client?.address
|
||||
|
||||
/datum/admins/proc/alert_2fa_necessary(client/client)
|
||||
var/msg = " is trying to join, but needs to verify their ckey."
|
||||
message_admins("[key_name_admin(client)][msg]")
|
||||
log_admin("[key_name(client)][msg]")
|
||||
|
||||
for (var/client/admin_client as anything in GLOB.admins)
|
||||
if (admin_client == client)
|
||||
continue
|
||||
|
||||
if (!check_rights_for(admin_client, R_PERMISSIONS))
|
||||
continue
|
||||
|
||||
to_chat(
|
||||
admin_client,
|
||||
type = MESSAGE_TYPE_ADMINLOG,
|
||||
html = "<span class='admin'><span class='prefix'>ADMIN 2FA:</span> You have the ability to verify [key_name_admin(client)] by using the Permissions Panel.</span>",
|
||||
confidential = TRUE,
|
||||
)
|
||||
|
||||
/datum/admins/vv_edit_var(var_name, var_value)
|
||||
return FALSE //nice try trialmin
|
||||
|
||||
@@ -210,3 +385,6 @@ you will have to do something like if(client.rights & R_ADMIN) yourself.
|
||||
|
||||
/proc/HrefTokenFormField(forceGlobal = FALSE)
|
||||
return "<input type='hidden' name='admin_token' value='[RawHrefToken(forceGlobal)]'>"
|
||||
|
||||
#undef RESULT_2FA_VALID
|
||||
#undef RESULT_2FA_ID
|
||||
|
||||
@@ -119,8 +119,13 @@
|
||||
deadminlink = " <a class='small' href='?src=[REF(src)];[HrefToken()];editrights=activate;key=[adm_ckey]'>\[RA\]</a>"
|
||||
else
|
||||
deadminlink = " <a class='small' href='?src=[REF(src)];[HrefToken()];editrights=deactivate;key=[adm_ckey]'>\[DA\]</a>"
|
||||
|
||||
var/verify_link = ""
|
||||
if (D.blocked_by_2fa)
|
||||
verify_link += " | <a class='small' href='?src=[REF(src)];[HrefToken()];editrights=verify;key=[adm_ckey]'>\[2FA VERIFY\]</a>"
|
||||
|
||||
output += "<tr>"
|
||||
output += "<td style='text-align:center;'>[adm_ckey]<br>[deadminlink]<a class='small' href='?src=[REF(src)];[HrefToken()];editrights=remove;key=[adm_ckey]'>\[-\]</a><a class='small' href='?src=[REF(src)];[HrefToken()];editrights=sync;key=[adm_ckey]'>\[SYNC TGDB\]</a></td>"
|
||||
output += "<td style='text-align:center;'>[adm_ckey]<br>[deadminlink]<a class='small' href='?src=[REF(src)];[HrefToken()];editrights=remove;key=[adm_ckey]'>\[-\]</a><a class='small' href='?src=[REF(src)];[HrefToken()];editrights=sync;key=[adm_ckey]'>\[SYNC TGDB\]</a>[verify_link]</td>"
|
||||
output += "<td><a href='?src=[REF(src)];[HrefToken()];editrights=rank;key=[adm_ckey]'>[D.rank.name]</a></td>"
|
||||
output += "<td><a class='small' href='?src=[REF(src)];[HrefToken()];editrights=permissions;key=[adm_ckey]'>[rights2text(D.rank.include_rights," ")]</a></td>"
|
||||
output += "<td><a class='small' href='?src=[REF(src)];[HrefToken()];editrights=permissions;key=[adm_ckey]'>[rights2text(D.rank.exclude_rights," ", "-")]</a></td>"
|
||||
@@ -148,7 +153,7 @@
|
||||
var/task = href_list["editrights"]
|
||||
var/skip
|
||||
var/legacy_only
|
||||
if(task == "activate" || task == "deactivate" || task == "sync")
|
||||
if(task == "activate" || task == "deactivate" || task == "sync" || task == "verify")
|
||||
skip = TRUE
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) && CONFIG_GET(flag/protect_legacy_admins) && task == "rank")
|
||||
if(admin_ckey in GLOB.protected_admins)
|
||||
@@ -204,6 +209,13 @@
|
||||
force_deadmin(admin_key, D)
|
||||
if("sync")
|
||||
sync_lastadminrank(admin_ckey, admin_key, D)
|
||||
if("verify")
|
||||
var/msg = "has authenticated [admin_ckey]"
|
||||
message_admins("[key_name_admin(usr)] [msg]")
|
||||
log_admin("[key_name(usr)] [msg]")
|
||||
|
||||
D.bypass_2fa = TRUE
|
||||
D.associate(GLOB.directory[admin_ckey])
|
||||
edit_admin_permissions()
|
||||
|
||||
/datum/admins/proc/add_admin(admin_ckey, admin_key, use_db)
|
||||
@@ -383,10 +395,13 @@
|
||||
if(D) //they were previously an admin
|
||||
D.disassociate() //existing admin needs to be disassociated
|
||||
D.rank = R //set the admin_rank as our rank
|
||||
D.bypass_2fa = TRUE // Another admin has cleared us
|
||||
var/client/C = GLOB.directory[admin_ckey]
|
||||
D.associate(C)
|
||||
else
|
||||
D = new(R, admin_ckey, TRUE) //new admin
|
||||
D = new(R, admin_ckey) //new admin
|
||||
D.bypass_2fa = TRUE // Another admin has cleared us
|
||||
D.activate()
|
||||
message_admins(m1)
|
||||
log_admin(m2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user