Mercurial > prosody-modules
changeset 6326:2623515d8507
mod_protect_last_admin: New module to ensure the last admin account on a server cannot be demoted or removed
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 25 Sep 2025 18:21:27 +0100 |
| parents | ba2c344e1bdd |
| children | 4f6fb56572f7 |
| files | mod_protect_last_admin/README.md mod_protect_last_admin/mod_protect_last_admin.lua |
| diffstat | 2 files changed, 96 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_protect_last_admin/README.md Thu Sep 25 18:21:27 2025 +0100 @@ -0,0 +1,46 @@ +--- +labels: +- 'Stage-Beta' +summary: Prevent the last admin account from being deleted or demoted +... + +Introduction +============ + +This module ensures there is always one administrator (or one account with the +specified roles, which can be configured). It prevents a situation where the +only admin on a server deletes their account or switches to another role, +leaving the server in a state where it has no administrator and no +administrator can be created with manual intervention (e.g. using prosodyctl +to set someone as an admin). + +Configuration +============= + +Simply load the module as usual: + +``` {.lua} +modules_enabled = { + ... + "protect_last_admin"; + ... +} +``` + +By default the module ensures there is at least one account with the +`prosody:operator` or `prosody:admin` role. You can specify the list of roles +using the `protect_last_admin_roles` setting in the configuration: + +``` {.lua} +-- Ensure there is always one prosody:operator +protect_last_admin_roles = { "prosody:operator" } +``` + +Compatibility +============= + + ----- ------- + trunk Works (requires commit [01f95f3de6fc](https://hg.prosody.im/trunk/rev/01f95f3de6fc) from 2025-09-25) + ----- ------- + 13.0 Not compatible + ---- -------
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_protect_last_admin/mod_protect_last_admin.lua Thu Sep 25 18:21:27 2025 +0100 @@ -0,0 +1,50 @@ +module:set_global(); + +local um = require "prosody.core.usermanager"; + +local set = require "prosody.util.set"; + +local protected_roles = module:get_option_set("protect_last_admin_roles", { "prosody:admin", "prosody:operator" }); + +local function other_protected_users(username, host) + local protected_users = set.new(); + for protected_role in protected_roles do + local users = um.get_users_with_role(protected_role, host); + module:log("debug", "Role %s: %s", protected_role, require "util.serialization".serialize(users, "debug")); + protected_users:add_list(um.get_users_with_role(protected_role, host)); + end + protected_users:remove(username); + return not protected_users:empty(); +end + +function check_last_admin(event) + local username, host = event.username, event.host; + + local current_role = um.get_user_role(username, host); + if not current_role then + module:log("warn", "Couldn't detect role for %s@%s", username, host); + return; + end + + module:log("debug", "Checking whether to allow this event..."); + + if event.role_name and protected_roles:contains(event.role_name) then + module:log("debug", "Permitting role change to %s", event.role_name); + return; -- Switching to another protected role is fine + end + + if not protected_roles:contains(current_role.name) then + module:log("debug", "Permitting event, role %s is not protected", current_role.name); + return; -- This is not a protected user + end + + if not other_protected_users(username, host) then + event.reason = "Cannot remove the only administrator"; + return false; + end + module:log("debug", "Permitting event, no reason not to!"); +end + +module:hook("pre-delete-user", check_last_admin); +module:hook("pre-disable-user", check_last_admin); +module:hook("pre-user-role-change", check_last_admin);
