Auth System Changes (#21698)

Changes the Auth System to use the new login server to fetch the
users/groups instead of the deprecated forumuserapi.

Removes the ForumUserAPI and moves the admin authorization processes to
a auth subsystem

---------

Co-authored-by: Werner <Arrow768@users.noreply.github.com>
This commit is contained in:
Werner
2026-01-23 18:43:28 +00:00
committed by GitHub
co-authored by Werner
parent 67ab8d569a
commit af9565bc06
17 changed files with 635 additions and 331 deletions
+1 -225
View File
@@ -1,6 +1,3 @@
var/list/admin_ranks = list() //list of all ranks with associated rights
var/list/forum_groupids_to_ranks = list()
/datum/admin_rank
var/rank_name
var/group_id
@@ -9,225 +6,4 @@ var/list/forum_groupids_to_ranks = list()
/datum/admin_rank/New(raw_data)
group_id = raw_data["role_id"]
rank_name = raw_data["name"]
rights = auths_to_rights(raw_data["auths"])
/datum/admin_rank/proc/auths_to_rights(list/auths)
. = 0
for (var/auth in auths)
switch (lowertext(auth))
if ("r_buildmode","r_build")
. |= R_BUILDMODE
if ("r_admin")
. |= R_ADMIN
if ("r_ban")
. |= R_BAN
if ("r_fun")
. |= R_FUN
if ("r_server")
. |= R_SERVER
if ("r_debug")
. |= R_DEBUG
if ("r_permissions","r_rights")
. |= R_PERMISSIONS
if ("r_possess")
. |= R_POSSESS
if ("r_stealth")
. |= R_STEALTH
if ("r_rejuv","r_rejuvenate")
. |= R_REJUVENATE
if ("r_varedit")
. |= R_VAREDIT
if ("r_sound","r_sounds")
. |= R_SOUNDS
if ("r_spawn","r_create")
. |= R_SPAWN
if ("r_moderator")
. |= R_MOD
if ("r_developer")
. |= R_DEV
if ("r_cciaa")
. |= R_CCIAA
if ("r_everything","r_host","r_all")
. |= (R_BUILDMODE | R_ADMIN | R_BAN | R_FUN | R_SERVER | R_DEBUG | R_PERMISSIONS | R_POSSESS | R_STEALTH | R_REJUVENATE | R_VAREDIT | R_SOUNDS | R_SPAWN | R_MOD | R_CCIAA | R_DEV)
else
crash_with("Unknown rank in file: [auth]")
/proc/load_admin_ranks(rank_file="config/admin_ranks.json")
admin_ranks.Cut()
forum_groupids_to_ranks.Cut()
if(!(rustg_file_exists(rank_file) == "true"))
log_config("The file [rank_file] does not exist, unable to load the admin ranks.")
return
var/list/data = json_decode(file2text(rank_file))
for (var/list/group in data)
var/datum/admin_rank/rank = new(group)
admin_ranks[rank.rank_name] = rank
forum_groupids_to_ranks["[rank.group_id]"] = rank
/hook/startup/proc/loadAdmins()
load_admin_ranks()
load_admins()
return 1
/proc/load_admins()
clear_admins()
if(GLOB.config.admin_legacy_system)
//load text from file
var/list/Lines = file2list("config/admins.txt")
//process each line seperately
for(var/line in Lines)
if(!length(line))
continue
if(copytext(line,1,2) == "#")
continue
//Split the line at every "-"
var/list/List = text2list(line, "-")
if(List.len != 2)
continue
//ckey is before the first "-"
var/ckey = ckey(List[1])
if(!ckey)
continue
//rank follows the first "-"
var/rank = trim(List[2])
//load permissions associated with this rank
var/datum/admin_rank/rank_object = admin_ranks[rank]
if (!rank_object)
log_world("ERROR: Unrecognized rank in admins.txt: \"[rank]\"")
continue
//create the admin datum and store it for later use
var/datum/admins/D = new /datum/admins(rank, rank_object?.rights || 0, ckey)
//find the client for a ckey if they are connected and associate them with the new admin datum
D.associate(GLOB.directory[ckey])
LOG_DEBUG("AdminRanks: Updated Admins from Legacy System")
else
//The current admin system uses SQL
if(!establish_db_connection(GLOB.dbcon))
log_world("ERROR: AdminRanks: Failed to connect to database in load_admins(). Reverting to legacy system.")
log_misc("AdminRanks: Failed to connect to database in load_admins(). Reverting to legacy system.")
GLOB.config.admin_legacy_system = 1
load_admins()
return
var/DBQuery/query = GLOB.dbcon.NewQuery("SELECT ckey, `rank`, flags FROM ss13_admins;")
query.Execute()
while(query.NextRow())
var/ckey = query.item[1]
var/rank = query.item[2]
var/rights = query.item[3]
if(istext(rights))
rights = text2num(rights)
var/datum/admins/D = new /datum/admins(rank, rights, ckey)
//find the client for a ckey if they are connected and associate them with the new admin datum
D.associate(GLOB.directory[ckey])
if(!admin_datums)
log_world("ERROR: AdminRanks: The database query in load_admins() resulted in no admins being added to the list. Reverting to legacy system.")
log_misc("AdminRanks: The database query in load_admins() resulted in no admins being added to the list. Reverting to legacy system.")
GLOB.config.admin_legacy_system = 1
load_admins()
return
#ifdef TESTING
var/msg = "Admins Built:\n"
for(var/ckey in admin_datums)
var/rank
var/datum/admins/D = admin_datums[ckey]
if(D) rank = D.rank
msg += "\t[ckey] - [rank]\n"
testing(msg)
#endif
/proc/clear_admins()
//clear the datums references
admin_datums.Cut()
for(var/s in GLOB.staff)
var/client/C = s
C.remove_admin_verbs()
C.holder = null
GLOB.staff.Cut()
// Clears admins from the world config.
for (var/A in world.GetConfig("admin"))
world.SetConfig("APP/admin", A, null)
/proc/update_admins_from_api(reload_once_done=FALSE)
if (!establish_db_connection(GLOB.dbcon))
log_and_message_admins("AdminRanks: Failed to connect to database in update_admins_from_api(). Carrying on with old staff lists.")
return FALSE
var/list/admins_to_push = list()
for (var/rank_name in admin_ranks)
var/datum/admin_rank/rank = admin_ranks[rank_name]
var/datum/http_request/forumuser_api/req = new()
req.prepare_roles_query(rank.group_id)
req.begin_async()
UNTIL(req.is_complete())
var/datum/http_response/resp = req.into_response()
if (resp.errored)
crash_with("Role request errored for id [rank.group_id] with: [resp.error]")
log_and_message_admins("AdminRanks: Loading admins from forumuser API FAILED. Please alert web-service maintainers immediately!")
return FALSE
for (var/datum/forum_user/user in resp.body)
admins_to_push += user
var/DBQuery/prep_query = GLOB.dbcon.NewQuery("UPDATE ss13_admins SET status = 0")
prep_query.Execute()
for (var/user in admins_to_push)
insert_user_to_admins_table(user)
var/DBQuery/del_query = GLOB.dbcon.NewQuery("DELETE FROM ss13_admins WHERE status = 0")
del_query.Execute()
if (reload_once_done)
load_admins()
LOG_DEBUG("AdminRanks: Updated Admins from ForumUserAPI")
return TRUE
/proc/insert_user_to_admins_table(datum/forum_user/user)
if(isnull(user.ckey))
LOG_DEBUG("AdminRanks: [user.forum_name] does not have a ckey linked - Ignoring")
return
if(user.psync_game_disabled)
LOG_DEBUG("AdminRanks: [user.forum_name] has permsync-game disabled - Ignoring")
return
var/rights = 0
for (var/group_id in (user.forum_secondary_groups + user.forum_primary_group))
var/datum/admin_rank/r = forum_groupids_to_ranks["[group_id]"]
if (r)
rights |= r.rights
var/primary_rank = "Administrator"
if (forum_groupids_to_ranks["[user.forum_primary_group]"])
var/datum/admin_rank/r = forum_groupids_to_ranks["[user.forum_primary_group]"]
primary_rank = r.rank_name
var/DBQuery/query = GLOB.dbcon.NewQuery("INSERT INTO ss13_admins VALUES (:ckey:, :rank:, :flags:, 1) ON DUPLICATE KEY UPDATE rank = :rank:, flags = :flags:, status = 1")
query.Execute(list("ckey" = ckey(user.ckey), "rank" = primary_rank, "flags" = rights))
rights = SSauth.auths_to_rights(raw_data["auths"])
@@ -37,7 +37,7 @@
admins += list(d)
data["admins"] = admins
data["forumuserui_enabled"] = GLOB.config.use_forumuser_api
data["forumuserui_enabled"] = GLOB.config.use_authentik_api
return data
@@ -83,8 +83,8 @@
PRIVATE_PROC(TRUE)
var/new_rank
if(admin_ranks.len)
new_rank = input("Please select a rank", "New rank", null, null) as null|anything in (admin_ranks|"*New Rank*")
if(SSauth.admin_ranks.len)
new_rank = input("Please select a rank", "New rank", null, null) as null|anything in (SSauth.admin_ranks|"*New Rank*")
else
new_rank = input("Please select a rank", "New rank", null, null) as null|anything in list("Game Master","Game Admin", "Trial Admin", "Admin Observer","*New Rank*")
@@ -105,18 +105,18 @@
return
if(admin_ranks.len)
if(new_rank in admin_ranks)
new_admin_rank_datum = admin_ranks[new_rank] //we typed a rank which already exists, use its rights
if(SSauth.admin_ranks.len)
if(new_rank in SSauth.admin_ranks)
new_admin_rank_datum = SSauth.admin_ranks[new_rank] //we typed a rank which already exists, use its rights
else
admin_ranks[new_rank] = 0 //add the new rank to admin_ranks
SSauth.admin_ranks[new_rank] = 0 //add the new rank to SSauth.admin_ranks
else
new_admin_rank_datum = admin_ranks[new_rank] //we input an existing rank, use its rights
new_admin_rank_datum = SSauth.admin_ranks[new_rank] //we input an existing rank, use its rights
if(D)
D.disassociate() //remove adminverbs and unlink from client
D.rank = new_rank //update the rank
D.rights = new_admin_rank_datum.rights //update the rights based on admin_ranks (default: 0)
D.rights = new_admin_rank_datum.rights //update the rights based on SSauth.admin_ranks (default: 0)
else
D = new /datum/admins(new_rank, rights, admin_ckey)
+3 -3
View File
@@ -105,11 +105,11 @@
if(!check_rights(R_SERVER|R_DEV))
return
if (GLOB.config.use_forumuser_api)
update_admins_from_api(FALSE)
if (GLOB.config.use_authentik_api)
SSauth.update_admins_from_authentik(FALSE)
log_and_message_admins("manually reloaded admins.")
load_admins()
SSauth.load_admins()
feedback_add_details("admin_verb","RLDA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
//todo:
+1 -1
View File
@@ -453,7 +453,7 @@ GLOBAL_LIST_INIT(localhost_addresses, list(
to_chat_immediate(src, SPAN_ALERT("If the title screen is black, resources are still downloading. Please be patient until the title screen appears."))
var/local_connection = (GLOB.config.auto_local_admin && !GLOB.config.use_forumuser_api && (isnull(address) || GLOB.localhost_addresses[address]))
var/local_connection = (GLOB.config.auto_local_admin && !GLOB.config.use_authentik_api && (isnull(address) || GLOB.localhost_addresses[address]))
// Automatic admin rights for people connecting locally.
// Concept stolen from /tg/ with deepest gratitude.
// And ported from Nebula with love.
+189
View File
@@ -0,0 +1,189 @@
/**
* Authentik API
*
* This module handles the communication with the Authentik API.
*
* @see https://goauthentik.io/docs/api/
*/
/**
* Authentik Groups
*
* Groups are used to determine what rights a user has.
* The group with the highest priority is the primary group, which is used to determine the users's display rank.
*
* In Authentik the groups need to have the following attributes:
* - gameserver.sync: boolean - Whether the group should be synced to the gameserver.
* - gameserver.flags: list - The flags the group should have.
* - priority: number - The priority of the group. The group with the highest priority is the primary group.
*/
/datum/authentik_group
var/group_id // pk from API
var/group_name // name from API
var/sync_enabled // attributes.gameserver.sync
var/flags // attributes.gameserver.flags
var/priority // attributes.priority (default 0)
/datum/authentik_group/New(data)
// Parse API response
group_id = data["pk"]
group_name = data["name"]
// Parse attributes
var/list/attributes = data["attributes"]
if (attributes)
parse_attributes(attributes)
/datum/authentik_group/proc/parse_attributes(list/attributes)
PRIVATE_PROC(TRUE)
// Extract gameserver settings
if (attributes["gameserver"])
var/list/gameserver = attributes["gameserver"]
sync_enabled = gameserver["sync"] ? TRUE : FALSE
if (gameserver["flags"])
flags = gameserver["flags"]
else
flags = list()
else
sync_enabled = FALSE
flags = list()
// Extract priority
if (attributes["priority"])
priority = text2num("[attributes["priority"]]")
else
priority = 0
/**
* Authentik Users
*
* Need to have the following attributes:
* - accounts.byond.ckey: string - The ckey of the user.
*
* The groups of the users are mapped using the group_ids list, which contains the group IDs of the groups the user is in.
* The primary group is determined by the priority of the group. The group with the highest priority is the primary group.
* The permissions of all groups the user is in are combined to determine the users's permissions.
*/
/datum/authentik_user
var/user_id /// pk from API
var/username /// username from API
var/ckey /// attributes.accounts.byond.ckey
var/list/group_ids /// groups array from API
var/primary_group_id /// Determined by priority
var/primary_group_name /// Name of primary group
/datum/authentik_user/New(data)
// Parse API response
user_id = data["pk"]
username = data["username"]
// Parse attributes
var/list/attributes = data["attributes"]
if (attributes)
parse_attributes(attributes)
// Parse groups
if (data["groups"])
group_ids = data["groups"]
/datum/authentik_user/proc/parse_attributes(list/attributes)
PRIVATE_PROC(TRUE)
// Extract BYOND ckey from nested attributes
if (attributes["accounts"])
var/list/accounts = attributes["accounts"]
if (accounts["byond"])
var/list/byond = accounts["byond"]
ckey = byond["ckey"]
/**
* Determines the primary group of the user based on the priority of the groups.
* The group with the highest priority is the primary group.
*
* @returns string The name of the primary group.
*/
/datum/authentik_user/proc/determine_primary_group()
// Find group with highest priority
var/highest_priority = -1
var/datum/authentik_group/primary = null
for (var/datum/authentik_group/group in SSauth.authentik_groups)
if (group.group_id in group_ids)
if (group.priority > highest_priority)
highest_priority = group.priority
primary = group
if (primary)
primary_group_id = primary.group_id
primary_group_name = primary.group_name
else
primary_group_name = "Administrator" // Default
return primary_group_name
/**
* Aggregates the rights of all groups the user is in.
*
* @returns int The aggregated rights of the user.
*/
/datum/authentik_user/proc/aggregate_rights()
// Aggregate rights from all groups user belongs to
var/rights = 0
for (var/datum/authentik_group/group in SSauth.authentik_groups)
if (group.group_id in group_ids)
rights |= SSauth.auths_to_rights(group.flags)
return rights
/datum/http_request/authentik_api
/**
* Prepares a query to fetch groups from the Authentik API.
* Does not include users in the response.
*
* @param page The page number to fetch.
*/
/datum/http_request/authentik_api/proc/prepare_groups_query(page = 1)
var/url = "[GLOB.config.authentik_api_url]/core/groups/?page=[page]&include_users=false"
var/list/headers = list("Authorization" = "Bearer [GLOB.config.authentik_api_key]")
prepare(RUSTG_HTTP_METHOD_GET, url, headers=headers)
/**
* Prepares a query to fetch users from the Authentik API.
* Optionally filters by group IDs.
*
* @param list/group_ids List of group IDs to filter by.
* @param page The page number to fetch.
*/
/datum/http_request/authentik_api/proc/prepare_users_query(list/group_ids, page = 1)
// Build query string with group filters
var/group_filter = ""
if (length(group_ids))
for (var/i = 1; i <= length(group_ids); i++)
if (i > 1)
group_filter += "&"
group_filter += "groups_by_pk=[group_ids[i]]"
var/url = "[GLOB.config.authentik_api_url]/core/users/?[group_filter]&page=[page]"
var/list/headers = list("Authorization" = "Bearer [GLOB.config.authentik_api_key]")
prepare(RUSTG_HTTP_METHOD_GET, url, headers=headers)
/datum/http_request/authentik_api/into_response()
var/datum/http_response/R = ..()
if (R.errored)
return R
try
R.body = json_decode(R.body)
catch
R.errored = TRUE
R.error = "Malformed JSON returned."
return R
// Note: Authentik returns paginated results
// Body structure: {"pagination": {...}, "results": [...]}
// We need to handle pagination in the calling code
return R
-67
View File
@@ -1,67 +0,0 @@
var/global/forumuser_api_key = null
/datum/forum_user
var/forum_member_id
var/forum_name
var/forum_primary_group
var/list/forum_secondary_groups = list()
var/discord_id
var/ckey
var/psync_game_disabled = FALSE
/datum/forum_user/New(data)
forum_member_id = data["forum_member_id"]
forum_name = data["forum_name"]
forum_primary_group = data["forum_primary_group"]
for (var/id in splittext(data["forum_secondary_groups"], ","))
forum_secondary_groups += text2num(id)
discord_id = data["discord_id"]
ckey = data["ckey"]
if(data["psync_game_disabled"] == 1) //This here is needed because the data can be null, 1 or 0
psync_game_disabled = TRUE
/datum/http_request/forumuser_api
/datum/http_request/forumuser_api/proc/prepare_roles_query(role_id)
var/url = "[GLOB.config.forumuser_api_url]/staff/[role_id]"
var/list/headers = list("Authorization" = "Bearer [forumuser_api_key]")
prepare(RUSTG_HTTP_METHOD_GET, url, headers=headers)
/datum/http_request/forumuser_api/proc/prepare_user_discord(discord_id)
var/url = "[GLOB.config.forumuser_api_url]/user/discord/[discord_id]"
var/list/headers = list("Authorization" = "Bearer [forumuser_api_key]")
prepare(RUSTG_HTTP_METHOD_GET, url, headers=headers)
/datum/http_request/forumuser_api/proc/prepare_user_ckey(ckey)
var/url = "[GLOB.config.forumuser_api_url]/user/ckey/[ckey]"
var/list/headers = list("Authorization" = "Bearer [forumuser_api_key]")
prepare(RUSTG_HTTP_METHOD_GET, url, headers=headers)
/datum/http_request/forumuser_api/into_response()
var/datum/http_response/R = ..()
if (R.errored)
return R
try
R.body = json_decode(R.body)
catch
R.errored = TRUE
R.error = "Malformed JSON returned."
return R
var/list/users = list()
for (var/d in R.body)
users += new /datum/forum_user(d)
R.body = users
return R
@@ -130,8 +130,8 @@
/datum/topic_command/admins_reload/run_command(queryparams)
log_and_message_admins("AdminRanks: remote reload of the admins list initiated.")
if (GLOB.config.use_forumuser_api)
if (!update_admins_from_api(reload_once_done=FALSE))
if (GLOB.config.use_authentik_api)
if (!SSauth.update_admins_from_authentik(reload_once_done=TRUE))
statuscode = 500
response = "Updating admins from the forumuser API failed. Aborted."
return FALSE
@@ -141,6 +141,4 @@
else
statuscode = 200
response = "Admins reloaded."
load_admins()
return TRUE