New ban commands use private storage to keep ban information (#4201)

This commit is contained in:
Badlop 2024-04-19 13:47:06 +02:00
parent ce57e02769
commit 44bafa478e
1 changed files with 173 additions and 6 deletions

View File

@ -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) ->
%% <aa xmlns='bb'>Cluth</aa>
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} ->