-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
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 ;
local log = session.log or module._log ;
local query = stanza.tags [ 1 ] ;
if stanza.attr . type == " get " then
local reply = st.reply ( stanza ) ;
reply : tag ( " query " , { xmlns = " jabber:iq:register " } )
: tag ( " registered " ) : up ( )
: tag ( " username " ) : text ( session.username ) : up ( )
: tag ( " password " ) : up ( ) ;
session.send ( reply ) ;
else -- stanza.attr.type == "set"
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 , ... )
self.send ( st.reply ( stanza ) ) ;
return old_session_close ( self , ... ) ;
end
local old_session = capture_session_properties ( session ) ;
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 ( ) ;
} ) ;
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
session.send ( st.reply ( stanza ) ) ;
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
session.send ( st.error_reply ( stanza , " wait " , " internal-server-error " ) ) ;
end
else
session.send ( st.error_reply ( stanza , " modify " , " bad-request " ) ) ;
end
else
session.send ( st.error_reply ( stanza , " modify " , " bad-request " ) ) ;
end
end
end
return true ;
end
module : hook ( " iq/self/jabber:iq:register:query " , handle_registration_stanza ) ;
if compat then
module : hook ( " iq/host/jabber:iq:register:query " , function ( event )
local session , stanza = event.origin , event.stanza ;
if session.type == " c2s " and jid_bare ( stanza.attr . to ) == session.host then
return handle_registration_stanza ( event ) ;
end
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 ;
} ) ;