From 44bafa478e6da939413ba67d6b2470d27fea2bf0 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 19 Apr 2024 13:47:06 +0200 Subject: [PATCH] New ban commands use private storage to keep ban information (#4201) --- src/mod_admin_extra.erl | 179 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 886606b02..003d2b5de 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -49,7 +49,8 @@ % Accounts set_password/3, check_password_hash/4, delete_old_users/1, - delete_old_users_vhost/2, ban_account/3, check_password/3, + delete_old_users_vhost/2, check_password/3, + ban_account/3, ban_account_v2/3, get_ban_details/2, unban_account/2, % vCard set_nickname/3, get_vcard/3, @@ -85,6 +86,7 @@ -include("mod_roster.hrl"). -include("mod_privacy.hrl"). -include("ejabberd_sm.hrl"). +-include_lib("xmpp/include/scram.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%% @@ -236,8 +238,10 @@ get_commands_spec() -> "New password for user"], result = {res, rescode}, result_example = ok}, + #ejabberd_commands{name = ban_account, tags = [accounts], desc = "Ban an account: kick sessions and set random password", + longdesc = "This simply sets a random password.", module = ?MODULE, function = ban_account, args = [{user, binary}, {host, binary}, {reason, binary}], args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], @@ -245,6 +249,53 @@ get_commands_spec() -> "Reason for banning user"], result = {res, rescode}, result_example = ok}, + #ejabberd_commands{name = ban_account, tags = [accounts], + desc = "Ban an account", + longdesc = "This command kicks the account sessions, " + "sets a random password, and stores ban details in the " + "account private storage. " + "This command requires mod_private to be enabled. " + "Check also _`get_ban_details`_ API " + "and `_unban_account`_ API.", + module = ?MODULE, function = ban_account_v2, + version = 2, + note = "improved in 24.xx", + args = [{user, binary}, {host, binary}, {reason, binary}], + args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], + args_desc = ["User name to ban", "Server name", + "Reason for banning user"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = get_ban_details, tags = [accounts], + desc = "Get ban details about an account", + longdesc = "Check _`ban_account`_ API.", + module = ?MODULE, function = get_ban_details, + version = 2, + note = "added in 24.xx", + args = [{user, binary}, {host, binary}], + args_example = [<<"attacker">>, <<"myserver.com">>], + args_desc = ["User name to unban", "Server name"], + result = {ban_details, {list, + {detail, {tuple, [{name, string}, + {value, string} + ]}} + }}, + result_example = [{"reason", "Spamming other users"}, + {"bandate", "2024-04-22T09:16:47.975312Z"}, + {"lastdate", "2024-04-22T08:39:12Z"}, + {"lastreason", "Connection reset by peer"}]}, + #ejabberd_commands{name = unban_account, tags = [accounts], + desc = "Revert the ban from an account: set back the old password", + longdesc = "Check _`ban_account`_ API.", + module = ?MODULE, function = unban_account, + version = 2, + note = "added in 24.xx", + args = [{user, binary}, {host, binary}], + args_example = [<<"gooduser">>, <<"myserver.com">>], + args_desc = ["User name to unban", "Server name"], + result = {res, rescode}, + result_example = ok}, + #ejabberd_commands{name = num_resources, tags = [session], desc = "Get the number of resources of a user", module = ?MODULE, function = num_resources, @@ -905,7 +956,7 @@ set_password(User, Host, Password) -> check_password(User, Host, Password) -> ejabberd_auth:check_password(User, <<>>, Host, Password). -%% Copied some code from ejabberd_commands.erl +%% Copied some code from ejabberd_commands.erln check_password_hash(User, Host, PasswordHash, HashMethod) -> AccountPass = ejabberd_auth:get_password_s(User, Host), Methods = lists:map(fun(A) -> atom_to_binary(A, latin1) end, @@ -978,7 +1029,7 @@ delete_or_not(LUser, LServer, TimeStamp_oldest) -> end. %% -%% Ban account +%% Ban account v0 ban_account(User, Host, ReasonText) -> Reason = prepare_reason(ReasonText), @@ -1014,6 +1065,119 @@ prepare_reason([Reason]) -> prepare_reason(Reason) when is_binary(Reason) -> Reason. +%% +%% Ban account v2 + +ban_account_v2(User, Host, ReasonText) -> + case is_banned(User, Host) of + true -> + account_was_already_banned; + false -> + ban_account_v2_b(User, Host, ReasonText) + end. + +ban_account_v2_b(User, Host, ReasonText) -> + Reason = prepare_reason(ReasonText), + Pass = ejabberd_auth:get_password_s(User, Host), + Last = get_last(User, Host), + BanDate = xmpp_util:encode_timestamp(erlang:timestamp()), + BanPrivateXml = build_ban_xmlel(Reason, Pass, Last, BanDate), + ok = private_set2(User, Host, BanPrivateXml), + ok = set_random_password_v2(User, Host), + kick_sessions(User, Host, Reason), + ok. + +set_random_password_v2(User, Server) -> + NewPass = p1_rand:get_string(), + ok = ejabberd_auth:set_password(User, Server, NewPass). + +build_ban_xmlel(Reason, Pass, {LastDate, LastReason}, BanDate) -> + PassEls = build_pass_els(Pass), + #xmlel{name = <<"banned">>, + attrs = [{<<"xmlns">>, <<"ejabberd:banned">>}], + children = [#xmlel{name = <<"reason">>, attrs = [], children = [{xmlcdata, Reason}]}, + #xmlel{name = <<"password">>, attrs = [], children = PassEls}, + #xmlel{name = <<"lastdate">>, attrs = [], children = [{xmlcdata, LastDate}]}, + #xmlel{name = <<"lastreason">>, attrs = [], children = [{xmlcdata, LastReason}]}, + #xmlel{name = <<"bandate">>, attrs = [], children = [{xmlcdata, BanDate}]} + ]}. + +build_pass_els(Pass) when is_binary(Pass) -> + [{xmlcdata, Pass}]; +build_pass_els(#scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount}) -> + [#xmlel{name = <<"storedkey">>, attrs = [], children = [{xmlcdata, StoredKey}]}, + #xmlel{name = <<"serverkey">>, attrs = [], children = [{xmlcdata, ServerKey}]}, + #xmlel{name = <<"salt">>, attrs = [], children = [{xmlcdata, Salt}]}, + #xmlel{name = <<"hash">>, attrs = [], children = [{xmlcdata, misc:atom_to_binary(Hash)}]}, + #xmlel{name = <<"iterationcount">>, attrs = [], children = [{xmlcdata, integer_to_binary(IterationCount)}]} + ]. + +%% +%% Get ban details + +get_ban_details(User, Host) -> + [El] = private_get2(User, Host, <<"banned">>, <<"ejabberd:banned">>), + Reason = fxml:get_subtag_cdata(El, <<"reason">>), + LastDate = fxml:get_subtag_cdata(El, <<"lastdate">>), + LastReason = fxml:get_subtag_cdata(El, <<"lastreason">>), + BanDate = fxml:get_subtag_cdata(El, <<"bandate">>), + [{"reason", Reason}, + {"bandate", BanDate}, + {"lastdate", LastDate}, + {"lastreason", LastReason} + ]. + +is_banned(User, Host) -> + case lists:keyfind("bandate", 1, get_ban_details(User, Host)) of + {_, BanDate} when BanDate /= <<>> -> + true; + _ -> + false + end. + +%% +%% Unban account + +unban_account(User, Host) -> + case is_banned(User, Host) of + false -> + account_was_not_banned; + true -> + unban_account2(User, Host) + end. + +unban_account2(User, Host) -> + OldPass = get_oldpass(User, Host), + ok = ejabberd_auth:set_password(User, Host, OldPass), + UnBanPrivateXml = build_unban_xmlel(), + private_set2(User, Host, UnBanPrivateXml). + +get_oldpass(User, Host) -> + [El] = private_get2(User, Host, <<"banned">>, <<"ejabberd:banned">>), + Pass = fxml:get_subtag(El, <<"password">>), + get_pass(Pass). + +get_pass(#xmlel{children = [{xmlcdata, Pass}]}) -> + Pass; +get_pass(#xmlel{children = ScramEls} = Pass) when is_list(ScramEls) -> + StoredKey = fxml:get_subtag_cdata(Pass, <<"storedkey">>), + ServerKey = fxml:get_subtag_cdata(Pass, <<"serverkey">>), + Salt = fxml:get_subtag_cdata(Pass, <<"salt">>), + Hash = fxml:get_subtag_cdata(Pass, <<"hash">>), + IterationCount = fxml:get_subtag_cdata(Pass, <<"iterationcount">>), + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + hash = binary_to_existing_atom(Hash, latin1), + iterationcount = binary_to_integer(IterationCount)}. + +build_unban_xmlel() -> + #xmlel{name = <<"banned">>, attrs = [{<<"xmlns">>, <<"ejabberd:banned">>}]}. + %%% %%% Sessions %%% @@ -1515,11 +1679,14 @@ set_last(User, Server, Timestamp, Status) -> %% Cluth private_get(Username, Host, Element, Ns) -> - ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]}, - Els = mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host), - [{Ns, ElementXml}]), + Els = private_get2(Username, Host, Element, Ns), binary_to_list(fxml:element_to_binary(xmpp:encode(#private{sub_els = Els}))). +private_get2(Username, Host, Element, Ns) -> + ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]}, + mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host), + [{Ns, ElementXml}]). + private_set(Username, Host, ElementString) -> case fxml_stream:parse_element(ElementString) of {error, Error} ->