diff options
Diffstat (limited to 'plugins/mod_user_account_management.lua')
-rw-r--r-- | plugins/mod_user_account_management.lua | 181 |
1 files changed, 166 insertions, 15 deletions
diff --git a/plugins/mod_user_account_management.lua b/plugins/mod_user_account_management.lua index 130ed089..c2a0e3a2 100644 --- a/plugins/mod_user_account_management.lua +++ b/plugins/mod_user_account_management.lua @@ -7,16 +7,28 @@ -- -local st = require "util.stanza"; -local usermanager_set_password = require "core.usermanager".set_password; -local usermanager_delete_user = require "core.usermanager".delete_user; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local jid_bare = require "util.jid".bare; +local st = require "prosody.util.stanza"; +local usermanager = require "prosody.core.usermanager"; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local jid_bare, jid_node = import("prosody.util.jid", "bare", "node"); local compat = module:get_option_boolean("registration_compat", true); +local soft_delete_period = module:get_option_period("registration_delete_grace_period"); +local deleted_accounts = module:open_store("accounts_cleanup"); module:add_feature("jabber:iq:register"); +-- Allow us to 'freeze' a session and retrieve properties even after it is +-- destroyed +local function capture_session_properties(session) + return setmetatable({ + id = session.id; + ip = session.ip; + type = session.type; + client_id = session.client_id; + }, { __index = session }); +end + -- Password change and account deletion handler local function handle_registration_stanza(event) local session, stanza = event.origin, event.stanza; @@ -34,6 +46,12 @@ local function handle_registration_stanza(event) if query.tags[1] and query.tags[1].name == "remove" then local username, host = session.username, session.host; + if host ~= module.host then -- Sanity check for safety + module:log("error", "Host mismatch on deletion request (a bug): %s ~= %s", host, module.host); + session.send(st.error_reply(stanza, "cancel", "internal-server-error")); + return true; + end + -- This one weird trick sends a reply to this stanza before the user is deleted local old_session_close = session.close; session.close = function(self, ...) @@ -41,24 +59,57 @@ local function handle_registration_stanza(event) return old_session_close(self, ...); end - local ok, err = usermanager_delete_user(username, host); + local old_session = capture_session_properties(session); - if not ok then - log("debug", "Removing user account %s@%s failed: %s", username, host, err); - session.close = old_session_close; - session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); - return true; - end + if not soft_delete_period then + local ok, err = usermanager.delete_user(username, host); + + if not ok then + log("debug", "Removing user account %s@%s failed: %s", username, host, err); + session.close = old_session_close; + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; + end + + log("info", "User removed their account: %s@%s (deleted)", username, host); + module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = old_session }); + else + local ok, err = usermanager.disable_user(username, host, { + reason = "ibr"; + comment = "Deletion requested by user"; + when = os.time(); + }); - log("info", "User removed their account: %s@%s", username, host); - module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); + if not ok then + log("debug", "Removing (disabling) user account %s@%s failed: %s", username, host, err); + session.close = old_session_close; + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; + end + + local status = { + deleted_at = os.time(); + pending_until = os.time() + soft_delete_period; + client_id = session.client_id; + }; + deleted_accounts:set(username, status); + + log("info", "User removed their account: %s@%s (disabled, pending deletion)", username, host); + module:fire_event("user-deregistered-pending", { + username = username; + host = host; + source = "mod_register"; + session = old_session; + status = status; + }); + end else local username = query:get_child_text("username"); local password = query:get_child_text("password"); if username and password then username = nodeprep(username); if username == session.username then - if usermanager_set_password(username, password, session.host, session.resource) then + if usermanager.set_password(username, password, session.host, session.resource) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? @@ -85,3 +136,103 @@ if compat then end); end +-- This improves UX of soft-deleted accounts by informing the user that the +-- account has been deleted, rather than just disabled. They can e.g. contact +-- their admin if this was a mistake. +module:hook("authentication-failure", function (event) + if event.condition ~= "account-disabled" then return; end + local session = event.session; + local sasl_handler = session and session.sasl_handler; + if sasl_handler.username then + local status = deleted_accounts:get(sasl_handler.username); + if status then + event.text = "Account deleted"; + end + end +end, -1000); + +function restore_account(username) + local pending, pending_err = deleted_accounts:get(username); + if not pending then + return nil, pending_err or "Account not pending deletion"; + end + local account_info, err = usermanager.get_account_info(username, module.host); + if not account_info then + return nil, "Couldn't fetch account info: "..err; + end + local forget_ok, forget_err = deleted_accounts:set(username, nil); + if not forget_ok then + return nil, "Couldn't remove account from deletion queue: "..forget_err; + end + local enable_ok, enable_err = usermanager.enable_user(username, module.host); + if not enable_ok then + return nil, "Removed account from deletion queue, but couldn't enable it: "..enable_err; + end + return true, "Account restored"; +end + +-- Automatically clear pending deletion if an account is re-enabled +module:context("*"):hook("user-enabled", function (event) + if event.host ~= module.host then return; end + deleted_accounts:set(event.username, nil); +end); + +local cleanup_time = module:measure("cleanup", "times"); + +function cleanup_soft_deleted_accounts() + local cleanup_done = cleanup_time(); + local success, fail, restored, pending = 0, 0, 0, 0; + + for username in deleted_accounts:users() do + module:log("debug", "Processing account cleanup for '%s'", username); + local account_info, account_info_err = usermanager.get_account_info(username, module.host); + if not account_info then + module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, account_info_err); + fail = fail + 1; + else + if account_info.enabled == false then + local meta = deleted_accounts:get(username); + if meta.pending_until <= os.time() then + local ok, err = usermanager.delete_user(username, module.host); + if not ok then + module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, err); + fail = fail + 1; + else + success = success + 1; + deleted_accounts:set(username, nil); + module:log("debug", "Deleted account '%s' successfully", username); + module:fire_event("user-deregistered", { username = username, host = module.host, source = "mod_register" }); + end + else + pending = pending + 1; + end + else + module:log("warn", "Account '%s' is not disabled, removing from deletion queue", username); + restored = restored + 1; + end + end + end + + module:log("debug", "%d accounts scheduled for future deletion", pending); + + if success > 0 or fail > 0 then + module:log("info", "Completed account cleanup - %d accounts deleted (%d failed, %d restored, %d pending)", success, fail, restored, pending); + end + cleanup_done(); +end + +module:daily("Remove deleted accounts", cleanup_soft_deleted_accounts); + +--- shell command +module:add_item("shell-command", { + section = "user"; + name = "restore"; + desc = "Restore a user account scheduled for deletion"; + args = { + { name = "jid", type = "string" }; + }; + host_selector = "jid"; + handler = function (self, jid) --luacheck: ignore 212/self + return restore_account(jid_node(jid)); + end; +}); |