From 079be4de1c198d9397230757537aef1fef9060bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Tue, 21 Jun 2022 09:28:34 +0200 Subject: [PATCH 001/117] Eliminate mongoose_commands from mongoose_client_api_contacts Motivation: This is the only mongoose_client_api_* module, which used mongoose_commands, and we are moving away from this solution towards a GraphQL-oriented one. Thus, the (soon to be deprecated) REST API needs to be simplified and use the *_api modules directly. Main changes: - Added direct calls to mod_roster_api to mongoose_client_api_contacts - Added type specs for request-handling functions - Disabled client contacts API in mod_commands --- src/mod_commands.erl | 5 - .../mongoose_client_api_contacts.erl | 206 +++++++++++------- 2 files changed, 129 insertions(+), 82 deletions(-) diff --git a/src/mod_commands.erl b/src/mod_commands.erl index b4b5b100163..93861954550 100644 --- a/src/mod_commands.erl +++ b/src/mod_commands.erl @@ -125,7 +125,6 @@ commands() -> {module, ?MODULE}, {function, list_contacts}, {action, read}, - {security_policy, [user]}, {args, [{caller, binary}]}, {result, []} ], @@ -136,7 +135,6 @@ commands() -> {module, ?MODULE}, {function, add_contact}, {action, create}, - {security_policy, [user]}, {args, [{caller, binary}, {jid, binary}]}, {result, ok} ], @@ -147,7 +145,6 @@ commands() -> {module, ?MODULE}, {function, subscription}, {action, update}, - {security_policy, [user]}, {identifiers, [caller, jid]}, % caller has to be in identifiers, otherwise it breaks admin rest api {args, [{caller, binary}, {jid, binary}, {action, binary}]}, @@ -172,7 +169,6 @@ commands() -> {module, ?MODULE}, {function, delete_contact}, {action, delete}, - {security_policy, [user]}, {args, [{caller, binary}, {jid, binary}]}, {result, ok} ], @@ -184,7 +180,6 @@ commands() -> {module, ?MODULE}, {function, delete_contacts}, {action, delete}, - {security_policy, [user]}, {args, [{caller, binary}, {jids, [binary]}]}, {result, []} ], diff --git a/src/mongoose_client_api/mongoose_client_api_contacts.erl b/src/mongoose_client_api/mongoose_client_api_contacts.erl index 8bcb4228b0c..2d13aea9719 100644 --- a/src/mongoose_client_api/mongoose_client_api_contacts.erl +++ b/src/mongoose_client_api/mongoose_client_api_contacts.erl @@ -16,9 +16,8 @@ -ignore_xref([from_json/2, to_json/2, trails/0, forbidden_request/2]). --include("mongoose.hrl"). --include("jlib.hrl"). --include_lib("exml/include/exml.hrl"). +-type req() :: cowboy_req:req(). +-type state() :: map(). trails() -> mongoose_client_api_contacts_doc:trails(). @@ -43,26 +42,26 @@ allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}. +-spec forbidden_request(req(), state()) -> {stop, req(), state()}. forbidden_request(Req, State) -> Req1 = cowboy_req:reply(403, Req), {stop, Req1, State}. -to_json(Req, #{jid := Caller} = State) -> - CJid = jid:to_binary(Caller), +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> Method = cowboy_req:method(Req), Jid = cowboy_req:binding(jid, Req), case Jid of undefined -> - {ok, Res} = handle_request(Method, Jid, undefined, CJid, State), - {jiffy:encode(lists:flatten([Res])), Req, State}; + {ok, Res} = handle_request(Method, State), + {jiffy:encode(Res), Req, State}; _ -> Req2 = cowboy_req:reply(404, Req), {stop, Req2, State} end. - -from_json(Req, #{jid := Caller} = State) -> - CJid = jid:to_binary(Caller), +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> Method = cowboy_req:method(Req), {ok, Body, Req1} = cowboy_req:read_body(Req), case mongoose_client_api:json_to_map(Body) of @@ -73,49 +72,48 @@ from_json(Req, #{jid := Caller} = State) -> _ -> undefined end, Action = maps:get(<<"action">>, JSONData, undefined), - handle_request_and_respond(Method, Jid, Action, CJid, Req1, State); + handle_request_and_respond(Method, Jid, Action, Req1, State); _ -> mongoose_client_api:bad_request(Req1, State) end. %% @doc Called for a method of type "DELETE" -delete_resource(Req, #{jid := Caller} = State) -> - CJid = jid:to_binary(Caller), +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> Jid = cowboy_req:binding(jid, Req), case Jid of undefined -> - handle_multiple_deletion(CJid, get_requested_contacts(Req), Req, State); + handle_multiple_deletion(get_requested_contacts(Req), Req, State); _ -> - handle_single_deletion(CJid, Jid, Req, State) + handle_single_deletion(Jid, Req, State) end. -handle_multiple_deletion(_, undefined, Req, State) -> +-spec handle_multiple_deletion(undefined | [jid:literal_jid()], req(), state()) -> + {true | stop, req(), state()}. +handle_multiple_deletion(undefined, Req, State) -> mongoose_client_api:bad_request(Req, State); -handle_multiple_deletion(CJid, ToDelete, Req, State) -> - case handle_request(<<"DELETE">>, ToDelete, undefined, CJid, State) of - {ok, NotDeleted} -> - RespBody = #{not_deleted => NotDeleted}, - Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), - Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2), - {true, Req3, State}; - Other -> - serve_failure(Other, Req, State) - end. - -handle_single_deletion(_, undefined, Req, State) -> +handle_multiple_deletion(ToDelete, Req, State = #{jid := CJid}) -> + NotDeleted = delete_contacts(CJid, ToDelete), + RespBody = #{not_deleted => NotDeleted}, + Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), + Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2), + {true, Req3, State}. + +-spec handle_single_deletion(undefined | jid:literal_jid(), req(), state()) -> + {true | stop, req(), state()}. +handle_single_deletion(undefined, Req, State) -> mongoose_client_api:bad_request(Req, State); -handle_single_deletion(CJid, ToDelete, Req, State) -> - case handle_request(<<"DELETE">>, ToDelete, undefined, CJid, State) of - ok -> - {true, Req, State}; - Other -> - serve_failure(Other, Req, State) - end. - -handle_request_and_respond(_, undefined, _, _, Req, State) -> +handle_single_deletion(ToDelete, Req, State = #{jid := CJid}) -> + ok = delete_contact(CJid, ToDelete), + {true, Req, State}. + +-spec handle_request_and_respond(Method :: binary(), jid:literal_jid() | undefined, + Action :: binary() | undefined, req(), state()) -> + {true | stop, req(), state()}. +handle_request_and_respond(_, undefined, _, Req, State) -> mongoose_client_api:bad_request(Req, State); -handle_request_and_respond(Method, Jid, Action, CJid, Req, State) -> - case handle_request(Method, to_binary(Jid), Action, CJid, State) of +handle_request_and_respond(Method, Jid, Action, Req, State) -> + case handle_request(Method, Jid, Action, State) of ok -> {true, Req, State}; not_implemented -> @@ -126,33 +124,22 @@ handle_request_and_respond(Method, Jid, Action, CJid, Req, State) -> {stop, Req2, State} end. -serve_failure(not_implemented, Req, State) -> - Req2 = cowboy_req:reply(501, Req), - {stop, Req2, State}; -serve_failure(not_found, Req, State) -> - Req2 = cowboy_req:reply(404, Req), - {stop, Req2, State}; -serve_failure({error, ErrorType, Msg}, Req, State) -> - ?LOG_ERROR(#{what => api_contacts_error, - text => <<"Error while serving http request. Return code 500">>, - error_type => ErrorType, msg => Msg, req => Req}), - Req2 = cowboy_req:reply(500, Req), - {stop, Req2, State}. - +-spec get_requested_contacts(req()) -> [jid:literal_jid()] | undefined. get_requested_contacts(Req) -> Body = get_whole_body(Req, <<"">>), case mongoose_client_api:json_to_map(Body) of {ok, #{<<"to_delete">> := ResultJids}} when is_list(ResultJids) -> - case [X || X <- ResultJids, is_binary(X)] =:= ResultJids of - true -> + case [X || X <- ResultJids, is_binary(X)] of + ResultJids -> ResultJids; - false -> + _ -> undefined end; _ -> undefined end. +-spec get_whole_body(req(), binary()) -> binary(). get_whole_body(Req, Acc) -> case cowboy_req:read_body(Req) of {ok, Data, _Req2} -> @@ -161,42 +148,107 @@ get_whole_body(Req, Acc) -> get_whole_body(Req2, <>) end. -handle_request(<<"GET">>, undefined, undefined, CJid, _State) -> - mongoose_commands:execute(CJid, list_contacts, #{caller => CJid}); -handle_request(<<"POST">>, Jid, undefined, CJid, _State) -> - mongoose_commands:execute(CJid, add_contact, #{caller => CJid, - jid => Jid}); -handle_request(<<"DELETE">>, Jids, _Action, CJid, _State) when is_list(Jids) -> - mongoose_commands:execute(CJid, delete_contacts, #{caller => CJid, - jids => Jids}); -handle_request(Method, Jid, Action, CJid, #{jid := CallerJid, creds := Creds}) -> +-spec handle_request(binary(), state()) -> {ok, [jiffy:json_object()]} | {error, any()}. +handle_request(<<"GET">>, #{jid := CallerJid}) -> + list_contacts(CallerJid). + +-spec handle_request(Method :: binary(), jid:literal_jid() | undefined, + Action :: binary() | undefined, state()) -> + ok | not_found | not_implemented | {error, any()}. +handle_request(<<"POST">>, Jid, undefined, #{jid := CallerJid}) -> + add_contact(CallerJid, Jid); +handle_request(Method, Jid, Action, #{jid := CallerJid, creds := Creds}) -> HostType = mongoose_credentials:host_type(Creds), case contact_exists(HostType, CallerJid, jid:from_binary(Jid)) of true -> - handle_contact_request(Method, Jid, Action, CJid); + handle_contact_request(Method, Jid, Action, CallerJid); false -> not_found end. handle_contact_request(<<"PUT">>, Jid, <<"invite">>, CJid) -> - mongoose_commands:execute(CJid, subscription, #{caller => CJid, - jid => Jid, action => atom_to_binary(subscribe, latin1)}); + subscription(CJid, Jid, atom_to_binary(subscribe, latin1)); handle_contact_request(<<"PUT">>, Jid, <<"accept">>, CJid) -> - mongoose_commands:execute(CJid, subscription, #{caller => CJid, - jid => Jid, action => atom_to_binary(subscribed, latin1)}); -handle_contact_request(<<"DELETE">>, Jid, undefined, CJid) -> - mongoose_commands:execute(CJid, delete_contact, #{caller => CJid, - jid => Jid}); + subscription(CJid, Jid, atom_to_binary(subscribed, latin1)); handle_contact_request(_, _, _, _) -> not_implemented. -to_binary(S) when is_binary(S) -> - S; -to_binary(S) -> - list_to_binary(S). - -spec contact_exists(mongooseim:host_type(), jid:jid(), jid:jid() | error) -> boolean(). contact_exists(_, _, error) -> false; contact_exists(HostType, CallerJid, Jid) -> LJid = jid:to_lower(Jid), Res = mod_roster:get_roster_entry(HostType, CallerJid, LJid, short), Res =/= does_not_exist andalso Res =/= error. + +%% Internal functions + +-spec list_contacts(jid:jid()) -> {ok, [jiffy:json_object()]} | {error, any()}. +list_contacts(Caller) -> + case mod_roster_api:list_contacts(Caller) of + {ok, Rosters} -> + {ok, lists:map(fun roster_info/1, Rosters)}; + {ErrorCode, _Msg} -> + {error, ErrorCode} + end. + +-spec roster_info(mod_roster:roster()) -> jiffy:json_object(). +roster_info(Roster) -> + #{jid := Jid, subscription := Sub, ask := Ask} = mod_roster:item_to_map(Roster), + #{jid => jid:to_binary(Jid), subscription => Sub, ask => Ask}. + +-spec add_contact(jid:jid(), jid:literal_jid()) -> ok | {error, any()}. +add_contact(Caller, JabberID) -> + add_contact(Caller, JabberID, <<>>, []). + +add_contact(CallerJid, Other, Name, Groups) -> + case jid:from_binary(Other) of + error -> + {error, invalid_jid}; + Jid -> + Res = mod_roster_api:add_contact(CallerJid, Jid, Name, Groups), + skip_result_msg(Res) + end. + +-spec delete_contacts(jid:jid(), [jid:literal_jid()]) -> [jid:literal_jid()]. +delete_contacts(Caller, ToDelete) -> + maybe_delete_contacts(Caller, ToDelete, []). + +maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted; +maybe_delete_contacts(Caller, [H | T], NotDeleted) -> + case delete_contact(Caller, H) of + ok -> + maybe_delete_contacts(Caller, T, NotDeleted); + _Error -> + maybe_delete_contacts(Caller, T, NotDeleted ++ [H]) + end. + +-spec delete_contact(jid:jid(), jid:literal_jid()) -> ok | {error, any()}. +delete_contact(CallerJID, Other) -> + case jid:from_binary(Other) of + error -> + {error, invalid_jid}; + Jid -> + Res = mod_roster_api:delete_contact(CallerJID, Jid), + skip_result_msg(Res) + end. + +-spec subscription(jid:jid(), jid:literal_jid(), binary()) -> ok | {error, any()}. +subscription(CallerJID, Other, Action) -> + case decode_action(Action) of + error -> + {error, {bad_request, <<"invalid action">>}}; + Act -> + case jid:from_binary(Other) of + error -> + {error, invalid_jid}; + Jid -> + Res = mod_roster_api:subscription(CallerJID, Jid, Act), + skip_result_msg(Res) + end + end. + +decode_action(<<"subscribe">>) -> subscribe; +decode_action(<<"subscribed">>) -> subscribed; +decode_action(_) -> error. + +skip_result_msg({ok, _Msg}) -> ok; +skip_result_msg({ErrCode, _Msg}) -> {error, ErrCode}. From 77ca7882271bd2a7af58ba6f81555ba72f0454b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 30 Jun 2022 13:17:08 +0200 Subject: [PATCH 002/117] Remove the legacy Client API mongoose_api_client was replaced by mongoose_client_api, but it was still possible to configure both, confusing the user. The only functionality lost by this change is the 'cahnge_password' command, which was undocumented anyway. We can readd it as 'mongoose_client_api_accounts' if needed. --- src/mod_commands.erl | 4 - src/mongoose_api_client.erl | 169 ----------------- src/mongoose_api_common.erl | 10 - .../mongoose_system_metrics_collector.erl | 2 +- test/commands_backend_SUITE.erl | 172 +----------------- test/common/config_parser_helper.erl | 4 +- test/config_parser_SUITE.erl | 4 - .../mongooseim-pgsql.toml | 4 - 8 files changed, 4 insertions(+), 365 deletions(-) delete mode 100644 src/mongoose_api_client.erl diff --git a/src/mod_commands.erl b/src/mod_commands.erl index 93861954550..6223a65a275 100644 --- a/src/mod_commands.erl +++ b/src/mod_commands.erl @@ -190,7 +190,6 @@ commands() -> {module, ?MODULE}, {function, send_message}, {action, create}, - {security_policy, [user]}, {args, [{caller, binary}, {to, binary}, {body, binary}]}, {result, ok} ], @@ -211,7 +210,6 @@ commands() -> {module, ?MODULE}, {function, get_recent_messages}, {action, read}, - {security_policy, [user]}, {args, [{caller, binary}]}, {optargs, [{before, integer, 0}, {limit, integer, 100}]}, {result, []} @@ -223,7 +221,6 @@ commands() -> {module, ?MODULE}, {function, get_recent_messages}, {action, read}, - {security_policy, [user]}, {args, [{caller, binary}, {with, binary}]}, {optargs, [{before, integer, 0}, {limit, integer, 100}]}, {result, []} @@ -235,7 +232,6 @@ commands() -> {module, ?MODULE}, {function, change_user_password}, {action, update}, - {security_policy, [user]}, {identifiers, [host, user]}, {args, [{host, binary}, {user, binary}, {newpass, binary}]}, {result, ok} diff --git a/src/mongoose_api_client.erl b/src/mongoose_api_client.erl deleted file mode 100644 index 0f26c58dcd5..00000000000 --- a/src/mongoose_api_client.erl +++ /dev/null @@ -1,169 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author ludwikbukowski -%%% @copyright (C) 2016, Erlang Solutions Ltd. -%%% -%%% @end -%%% Created : 19. Jul 2016 17:55 -%%%------------------------------------------------------------------- -%% @doc MongooseIM REST HTTP API for clients. -%% This module implements cowboy REST callbacks and -%% passes the requests on to the http api backend module. -%% It provides also client authorization mechanism - --module(mongoose_api_client). --author("ludwikbukowski"). --include("mongoose_api.hrl"). --include("jlib.hrl"). --include("mongoose.hrl"). - --behaviour(mongoose_http_handler). - -%% mongoose_http_handler callbacks --export([routes/1]). - --export([to_json/2, from_json/2]). - -%% API --export([is_authorized/2, - init/2, - allowed_methods/2, - content_types_provided/2, - content_types_accepted/2, - rest_terminate/2, - delete_resource/2]). - --ignore_xref([allowed_methods/2, content_types_accepted/2, content_types_provided/2, - cowboy_router_paths/2, delete_resource/2, from_json/2, init/2, - is_authorized/2, rest_terminate/2, to_json/2]). - --import(mongoose_api_common, [action_to_method/1, - method_to_action/1, - process_request/4, - error_response/4, - parse_request_body/1]). - -%%-------------------------------------------------------------------- -%% mongoose_http_handler callbacks -%%-------------------------------------------------------------------- - --spec routes(mongoose_http_handler:options()) -> mongoose_http_handler:routes(). -routes(#{path := BasePath}) -> - ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), - ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), - try - Commands = mongoose_commands:list(user), - [handler_path(BasePath, Command) || Command <- Commands] - catch - Class:Err:Stacktrace -> - ?LOG_ERROR(#{what => rest_getting_command_list_failed, - class => Class, reason => Err, stacktrace => Stacktrace}), - [] - end. - -%%-------------------------------------------------------------------- -%% cowboy_rest callbacks -%%-------------------------------------------------------------------- - - -init(Req, #{command_category := CommandCategory}) -> - Bindings = maps:to_list(cowboy_req:bindings(Req)), - State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(user), - bindings = Bindings, - command_category = CommandCategory}, - {cowboy_rest, Req, State}. - -allowed_methods(Req, #http_api_state{command_category = Name} = State) -> - CommandList = mongoose_commands:list(user, Name), - AllowedMethods = [action_to_method(mongoose_commands:action(Command)) - || Command <- CommandList], - {AllowedMethods, Req, State}. - -content_types_provided(Req, State) -> - CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], - {CTP, Req, State}. - -content_types_accepted(Req, State) -> - CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], - {CTA, Req, State}. - -rest_terminate(_Req, _State) -> - ok. - -is_authorized(Req, State) -> - AuthDetails = cowboy_req:parse_header(<<"authorization">>, Req), - do_authorize(AuthDetails, Req, State). - -%% @doc Called for a method of type "DELETE" -delete_resource(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Arity = length(B), - Cmds = mongoose_commands:list(user, Category, method_to_action(<<"DELETE">>), SubCategory), - [Command] = [C || C <- Cmds, arity(C) == Arity], - mongoose_api_common:process_request(<<"DELETE">>, Command, Req, State). - -%%-------------------------------------------------------------------- -%% internal funs -%%-------------------------------------------------------------------- - -%% @doc Called for a method of type "GET" -to_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Arity = length(B), - Cmds = mongoose_commands:list(user, Category, method_to_action(<<"GET">>), SubCategory), - [Command] = [C || C <- Cmds, arity(C) == Arity], - mongoose_api_common:process_request(<<"GET">>, Command, Req, State). - - -%% @doc Called for a method of type "POST" and "PUT" -from_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Method = cowboy_req:method(Req), - case parse_request_body(Req) of - {error, _R}-> - error_response(bad_request, ?BODY_MALFORMED, Req, State); - {Params, _} -> - Arity = length(B) + length(Params), - Cmds = mongoose_commands:list(user, Category, method_to_action(Method), SubCategory), - case [C || C <- Cmds, arity(C) == Arity] of - [Command] -> - process_request(Method, Command, {Params, Req}, State); - [] -> - error_response(not_found, ?ARGS_LEN_ERROR, Req, State) - end - end. - -arity(C) -> - % we don't have caller in bindings (we know it from authorisation), - % so it doesn't count when checking arity - Args = mongoose_commands:args(C), - length([N || {N, _} <- Args, N =/= caller]). - -do_authorize({basic, User, Password}, Req, State) -> - case jid:from_binary(User) of - error -> - make_unauthorized_response(Req, State); - JID -> - do_check_password(JID, Password, Req, State) - end; -do_authorize(_, Req, State) -> - make_unauthorized_response(Req, State). - -do_check_password(#jid{} = JID, Password, Req, State) -> - case ejabberd_auth:check_password(JID, Password) of - true -> - {true, Req, State#http_api_state{entity = jid:to_binary(JID)}}; - _ -> - make_unauthorized_response(Req, State) - end. - -make_unauthorized_response(Req, State) -> - {{false, <<"Basic realm=\"mongooseim\"">>}, Req, State}. - --spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t()) -> ejabberd_cowboy:route(). -handler_path(Base, Command) -> - {[Base, mongoose_api_common:create_user_url_path(Command)], - ?MODULE, #{command_category => mongoose_commands:category(Command)}}. - diff --git a/src/mongoose_api_common.erl b/src/mongoose_api_common.erl index a9a2c76fe89..7bb1fc39fe8 100644 --- a/src/mongoose_api_common.erl +++ b/src/mongoose_api_common.erl @@ -66,7 +66,6 @@ %% API -export([create_admin_url_path/1, - create_user_url_path/1, action_to_method/1, method_to_action/1, parse_request_body/1, @@ -102,13 +101,6 @@ create_admin_url_path_iodata(Command) -> ["/", mongoose_commands:category(Command), maybe_add_bindings(Command, admin), maybe_add_subcategory(Command)]. --spec create_user_url_path(mongoose_commands:t()) -> ejabberd_cowboy:path(). -create_user_url_path(Command) -> - iolist_to_binary(create_user_url_path_iodata(Command)). - -create_user_url_path_iodata(Command) -> - ["/", mongoose_commands:category(Command), maybe_add_bindings(Command, user)]. - -spec process_request(Method :: method(), Command :: mongoose_commands:t(), Req :: cowboy_req:req() | {list(), cowboy_req:req()}, @@ -309,8 +301,6 @@ add_bindings(Args, Entity) -> [add_bind(A, Entity) || A <- Args]. %% skip "caller" arg for frontend command -add_bind({caller, _}, user) -> - ""; add_bind({ArgName, _}, _Entity) -> lists:flatten(["/:", atom_to_list(ArgName)]); add_bind(Other, _) -> diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 771e9ac0f3c..7d7f5ce5589 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -126,7 +126,7 @@ get_api() -> [#{report_name => http_api, key => Api, value => enabled} || Api <- ApiList]. filter_unknown_api(ApiList) -> - AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_api_admin, mongoose_api_client, + AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_api_admin, mongoose_domain_handler, mod_bosh, mod_websockets], [Api || Api <- ApiList, lists:member(Api, AllowedToReport)]. diff --git a/test/commands_backend_SUITE.erl b/test/commands_backend_SUITE.erl index 1f2d089e86f..a32593743d7 100644 --- a/test/commands_backend_SUITE.erl +++ b/test/commands_backend_SUITE.erl @@ -14,9 +14,6 @@ %%%% suite configuration %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -client_module() -> - mongoose_api_client. - backend_module() -> mongoose_api_admin. @@ -25,8 +22,7 @@ all() -> {group, simple_backend}, {group, get_advanced_backend}, {group, post_advanced_backend}, - {group, delete_advanced_backend}, - {group, simple_client} + {group, delete_advanced_backend} ]. groups() -> @@ -76,16 +72,6 @@ groups() -> put_wrong_bind_name, put_wrong_param_name ] - }, - {simple_client, [sequence], - [ - get_simple_client, - get_two_args_client, - get_bad_auth, - post_simple_client, - put_simple_client, - delete_simple_client - ] } ]. @@ -336,137 +322,10 @@ put_wrong_param_name(_Config) -> {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), check_status_code(Response, 404). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% Client side tests -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -get_simple_client(_Config) -> - Arg = {arg1, <<"bob@localhost">>}, - Base = "/api/clients", - Username = <<"username@localhost">>, - Auth = {binary_to_list(Username), "secret"}, - ExpectedBody = get_simple_client_command(Username, element(2, Arg)), - {ok, Response} = request(create_path_with_binds(Base, [Arg]), "GET", {Auth, true}), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_two_args_client(_Config) -> - Arg1 = {other, <<"bob@localhost">>}, - Arg2 = {limit, 10}, - Base = "/api/message", - Username = <<"alice@localhost">>, - Auth = {binary_to_list(Username), "secret"}, - ExpectedBody = get_two_args_client_command(Username, element(2, Arg1), element(2, Arg2)), - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "GET", {Auth, true}), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_bad_auth(_Config) -> - Arg = {arg1, <<"bob@localhost">>}, - Base = "/api/clients", - Username = <<"username@localhost">>, - Auth = {binary_to_list(Username), "secret"}, - get_simple_client_command(Username, element(2, Arg)), - {ok, Response} = request(create_path_with_binds(Base, [Arg]), "GET", {Auth, false}), - check_status_code(Response, 401). - -post_simple_client(_Config) -> - Arg1 = {title, <<"Juliet's despair">>}, - Arg2 = {content, <<"If they do see thee, they will murder thee!">>}, - Base = <<"/api/ohmyromeo">>, - Username = <<"username@localhost">>, - Auth = {binary_to_list(Username), "secret"}, - Result = binary_to_list(post_simple_client_command(Username, element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Base, "POST", [Arg1, Arg2], {Auth, true}), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix() ++"/api/ohmyromeo/" ++ Result)). - -put_simple_client(_Config) -> - Arg = {password, <<"ilovepancakes">>}, - Base = <<"/api/superusers">>, - Username = <<"joe@localhost">>, - Auth = {binary_to_list(Username), "secretpassword"}, - put_simple_client_command(Username, element(2, Arg)), - {ok, Response} = request(Base, "PUT", [Arg], {Auth, true}), - check_status_code(Response, 204). - -delete_simple_client(_Config) -> - Arg = {name, <<"giant">>}, - Base = "/api/bikes", - Username = <<"username@localhost">>, - Auth = {binary_to_list(Username), "secret"}, - get_simple_client_command(Username, element(2, Arg)), - {ok, Response} = request(create_path_with_binds(Base, [Arg]), "DELETE", {Auth, true}), - check_status_code(Response, 204). - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -commands_client() -> - [ - [ - {name, get_simple_client}, - {category, <<"clients">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_simple_client_command}, - {action, read}, - {identifiers, []}, - {security_policy, [user]}, - {args, [{caller, binary}, {arg1, binary}]}, - {result, {result, binary}} - ], - [ - {name, get_two_args_client}, - {category, <<"message">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_two_args_client_command}, - {action, read}, - {identifiers, []}, - {security_policy, [user]}, - {args, [{caller, binary}, {other, binary}, {limit, integer}]}, - {result, {result, binary}} - ], - [ - {name, post_simple_client}, - {category, <<"ohmyromeo">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, post_simple_client_command}, - {action, create}, - {identifiers, []}, - {security_policy, [user]}, - {args, [{caller, binary}, {title, binary}, {content, binary}]}, - {result, {result, binary}} - ], - [ - {name, put_simple_client}, - {category, <<"superusers">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, put_simple_client_command}, - {action, update}, - {identifiers, [caller]}, - {security_policy, [user]}, - {args, [{caller, binary}, {password, binary}]}, - {result, ok} - ], - [ - {name, delete_simple_client}, - {category, <<"bikes">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, delete_simple_client_command}, - {action, delete}, - {identifiers, []}, - {security_policy, [user]}, - {args, [{caller, binary}, {name, binary}]}, - {result, ok} - ] - ]. - commands_admin() -> [ [ @@ -564,8 +423,7 @@ commands_admin() -> ]. commands_new() -> - commands_admin() ++ commands_client(). - + commands_admin(). %% admin command funs get_simple_command(<<"bob@localhost">>) -> @@ -590,22 +448,6 @@ put_advanced_command(Arg1, Arg2, Arg3, Arg4) when is_binary(Arg1) and is_binary( and is_integer(Arg3) and is_integer(Arg4) -> ok. -%% clients command funs -get_simple_client_command(_Caller, _SomeBinary) -> - <<"client bob is OK">>. - -get_two_args_client_command(_Caller, _SomeBinary, _SomeInteger) -> - <<"client2 bob is OK">>. - -post_simple_client_command(_Caller, _Title, _Content) -> - <<"new_resource">>. - -put_simple_client_command(_Username, _Password) -> - changed. - -delete_simple_client_command(_Username, _BikeName) -> - changed. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% utilities %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -627,9 +469,6 @@ maybe_add_accepted_headers(_) -> accepted_headers() -> [{<<"Content-Type">>, <<"application/json">>}, {<<"Accept">>, <<"application/json">>}]. -maybe_add_auth_header({User, Password}) -> - Basic = list_to_binary("Basic " ++ base64:encode_to_string(User ++ ":"++ Password)), - [{<<"authorization">>, Basic}]; maybe_add_auth_header(admin) -> []. @@ -665,13 +504,6 @@ do_request(Path, Method, Body, {headers, Headers}) -> teardown(), R. -request(Path, Method, BodyData, {{_User, _Pass} = Auth, Authorized}) -> - setup(client_module()), - meck:expect(ejabberd_auth, check_password, fun(_, _) -> Authorized end), - Body = maybe_add_body(BodyData), - AuthHeader = maybe_add_auth_header(Auth), - AcceptHeader = maybe_add_accepted_headers(Method), - do_request(Path, Method, Body, {headers, AuthHeader ++ AcceptHeader}); request(Path, Method, BodyData, admin) -> ct:pal("~p, ~p, ~p", [Path, Method, BodyData]), setup(backend_module()), diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 590df44c69a..c606560d246 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -172,9 +172,7 @@ options("mongooseim-pgsql") -> ping_rate => 120000, timeout => infinity}), config([listen, http, handlers, mongoose_api_admin], #{host => "localhost", path => "/api", - username => <<"ala">>, password => <<"makotaipsa">>}), - config([listen, http, handlers, mongoose_api_client], - #{host => "localhost", path => "/api/contacts/{:jid}"}) + username => <<"ala">>, password => <<"makotaipsa">>}) ], transport => #{num_acceptors => 10, max_connections => 1024}, tls => #{certfile => "priv/cert.pem", keyfile => "priv/dc1.pem", password => ""} diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 43d038b12de..fa1d795be88 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -99,7 +99,6 @@ groups() -> listen_http_handlers_client_api, listen_http_handlers_api, listen_http_handlers_api_admin, - listen_http_handlers_api_client, listen_http_handlers_domain, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, @@ -614,9 +613,6 @@ listen_http_handlers_api_admin(_Config) -> {P, T} = test_listen_http_handler(mongoose_api_admin), test_listen_http_handler_creds(P, T). -listen_http_handlers_api_client(_Config) -> - test_listen_http_handler(mongoose_api_client). - listen_http_handlers_domain(_Config) -> {P, T} = test_listen_http_handler(mongoose_domain_handler), test_listen_http_handler_creds(P, T). diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index a599aab8c0d..301b3d01c2d 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -44,10 +44,6 @@ username = "ala" password = "makotaipsa" - [[listen.http.handlers.mongoose_api_client]] - host = "localhost" - path = "/api/contacts/{:jid}" - [[listen.http.handlers.mod_bosh]] host = "_" path = "/http-bind" From ecd2cf364ba2ef0764aab2031da7683bf5d38b46 Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Fri, 26 Aug 2022 11:23:06 +0200 Subject: [PATCH 003/117] Domain admin tests in roster suite Added tests for domain admin in graphql_roster_SUITE. The tests are similar to those for a global admin, but they also check for permissions. --- big_tests/tests/graphql_roster_SUITE.erl | 135 ++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/big_tests/tests/graphql_roster_SUITE.erl b/big_tests/tests/graphql_roster_SUITE.erl index 508d62bca90..b61e29f519b 100644 --- a/big_tests/tests/graphql_roster_SUITE.erl +++ b/big_tests/tests/graphql_roster_SUITE.erl @@ -64,10 +64,33 @@ admin_roster_tests() -> ]. domain_admin_tests() -> - [domain_admin_subscribe_to_all_no_permission, + [admin_add_and_delete_contact, + admin_try_add_nonexistent_contact, + admin_try_add_contact_to_nonexistent_user, + domain_admin_try_add_contact_with_unknown_domain, + domain_admin_try_add_contact_no_permission, + admin_add_contacts, + admin_try_delete_nonexistent_contact, + domain_admin_try_delete_contact_with_unknown_domain, + domain_admin_try_delete_contact_no_permission, + admin_set_mutual_subscription, + domain_admin_set_mutual_subscription_try_connect_nonexistent_users, + domain_admin_set_mutual_subscription_try_connect_users_no_permission, + domain_admin_set_mutual_subscription_try_disconnect_nonexistent_users, + domain_admin_set_mutual_subscription_try_disconnect_users_no_permission, + domain_admin_subscribe_to_all_no_permission, admin_subscribe_to_all, + domain_admin_subscribe_to_all_with_wrong_user, domain_admin_subscribe_all_to_all_no_permission, - admin_subscribe_all_to_all]. + admin_subscribe_all_to_all, + domain_admin_subscribe_all_to_all_with_wrong_user, + admin_list_contacts, + domain_admin_list_contacts_wrong_user, + domain_admin_list_contacts_no_permission, + admin_get_contact, + domain_admin_get_contact_wrong_user, + domain_admin_get_contacts_no_permission + ]. init_per_suite(Config) -> Config1 = ejabberd_node_utils:init(mim(), Config), @@ -498,6 +521,61 @@ user_get_nonexistent_contact_story(Config, Alice) -> % Domain admin test cases +domain_admin_try_add_contact_with_unknown_domain(Config) -> + User = ?NONEXISTENT_DOMAIN_USER, + Contact = ?NONEXISTENT_USER2, + Res = admin_add_contact(User, Contact, Config), + get_unauthorized(Res). + +domain_admin_try_add_contact_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_try_add_contact_no_permission_story/3). + +domain_admin_try_add_contact_no_permission_story(Config, Alice, Bob) -> + Res = admin_add_contact(Alice, Bob, Config), + get_unauthorized(Res). + +domain_admin_try_delete_contact_with_unknown_domain(Config) -> + User = ?NONEXISTENT_DOMAIN_USER, + Res = admin_delete_contact(User, User, Config), + get_unauthorized(Res). + +domain_admin_try_delete_contact_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_try_delete_contact_no_permission_story/3). + +domain_admin_try_delete_contact_no_permission_story(Config, Alice, Bob) -> + Res = admin_delete_contact(Alice, Bob, Config), + get_unauthorized(Res). + +domain_admin_set_mutual_subscription_try_connect_nonexistent_users(Config) -> + Alice = ?NONEXISTENT_DOMAIN_USER, + Bob = ?NONEXISTENT_USER, + Res = admin_mutual_subscription(Alice, Bob, <<"CONNECT">>, Config), + get_unauthorized(Res). + +domain_admin_set_mutual_subscription_try_connect_users_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_set_mutual_subscription_try_connect_users_no_permission_story/3). + +domain_admin_set_mutual_subscription_try_connect_users_no_permission_story(Config, Alice, Bob) -> + Res = admin_mutual_subscription(Alice, Bob, <<"CONNECT">>, Config), + get_unauthorized(Res). + +domain_admin_set_mutual_subscription_try_disconnect_nonexistent_users(Config) -> + Alice = ?NONEXISTENT_DOMAIN_USER, + Bob = ?NONEXISTENT_USER, + Res = admin_mutual_subscription(Alice, Bob, <<"DISCONNECT">>, Config), + get_unauthorized(Res). + +domain_admin_set_mutual_subscription_try_disconnect_users_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_set_mutual_subscription_try_disconnect_users_no_permission_story/3). + +domain_admin_set_mutual_subscription_try_disconnect_users_no_permission_story(Config, Alice, Bob) -> + Res = admin_mutual_subscription(Alice, Bob, <<"DISCONNECT">>, Config), + get_unauthorized(Res). + domain_admin_subscribe_to_all_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice_bis, 1}], fun domain_admin_subscribe_to_all_no_permission/2). @@ -513,6 +591,59 @@ domain_admin_subscribe_all_to_all_no_permission(Config, Alice, Bob, Kate) -> Res = admin_subscribe_all_to_all([Alice, Bob, Kate], Config), get_unauthorized(Res). +domain_admin_subscribe_all_to_all_with_wrong_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_subscribe_all_to_all_with_wrong_user_story/3). + +domain_admin_subscribe_all_to_all_with_wrong_user_story(Config, Alice, Bob) -> + Kate = ?NONEXISTENT_DOMAIN_USER, + Res = admin_subscribe_all_to_all([Alice, Bob, Kate], Config), + get_unauthorized(Res). + +domain_admin_subscribe_to_all_with_wrong_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_subscribe_to_all_with_wrong_user_story/3). + +domain_admin_subscribe_to_all_with_wrong_user_story(Config, Alice, Bob) -> + Kate = ?NONEXISTENT_DOMAIN_USER, + Res = admin_subscribe_to_all(Alice, [Bob, Kate], Config), + check_if_created_succ(?SUBSCRIBE_TO_ALL_PATH, Res, [true, false]), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"does not exist">>)), + check_contacts([Bob], Alice), + check_contacts([Alice], Bob). + +domain_admin_list_contacts_wrong_user(Config) -> + % User with a non-existent domain + Res = admin_list_contacts(?NONEXISTENT_DOMAIN_USER, Config), + get_unauthorized(Res), + % Non-existent user with existent domain + Res2 = admin_list_contacts(?NONEXISTENT_USER, Config), + ?assertEqual([], get_ok_value(?LIST_CONTACTS_PATH, Res2)). + +domain_admin_list_contacts_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_list_contacts_no_permission_story/2). + +domain_admin_list_contacts_no_permission_story(Config, Alice) -> + Res = admin_list_contacts(Alice, Config), + get_unauthorized(Res). + +domain_admin_get_contact_wrong_user(Config) -> + % User with a non-existent domain + Res = admin_get_contact(?NONEXISTENT_DOMAIN_USER, ?NONEXISTENT_USER, Config), + get_unauthorized(Res), + % Non-existent user with existent domain + Res2 = admin_get_contact(?NONEXISTENT_USER, ?NONEXISTENT_USER, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"does not exist">>)). + +domain_admin_get_contacts_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_get_contacts_no_permission_story/3). + +domain_admin_get_contacts_no_permission_story(Config, Alice, Bob) -> + Res = admin_get_contact(Alice, Bob, Config), + get_unauthorized(Res). + % Helpers admin_add_contact(User, Contact, Config) -> From f84b41b1f8322dbe7efbc8c3eee762df87e2e96e Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Fri, 2 Sep 2022 15:03:14 +0200 Subject: [PATCH 004/117] Missing domain admin tests --- big_tests/tests/domain_helper.erl | 2 + big_tests/tests/graphql_account_SUITE.erl | 175 ++++++++++++- big_tests/tests/graphql_domain_SUITE.erl | 108 ++++++-- big_tests/tests/graphql_gdpr_SUITE.erl | 27 +- big_tests/tests/graphql_helper.erl | 9 +- big_tests/tests/graphql_http_upload_SUITE.erl | 37 ++- big_tests/tests/graphql_inbox_SUITE.erl | 44 +++- big_tests/tests/graphql_last_SUITE.erl | 108 +++++++- big_tests/tests/graphql_metric_SUITE.erl | 45 +++- big_tests/tests/graphql_muc_SUITE.erl | 98 +++++++- big_tests/tests/graphql_muc_light_SUITE.erl | 236 +++++++++++++++++- big_tests/tests/graphql_offline_SUITE.erl | 49 +++- big_tests/tests/graphql_private_SUITE.erl | 27 +- big_tests/tests/graphql_session_SUITE.erl | 127 +++++++++- big_tests/tests/graphql_stanza_SUITE.erl | 70 +++++- big_tests/tests/graphql_token_SUITE.erl | 21 +- big_tests/tests/graphql_vcard_SUITE.erl | 43 +++- 17 files changed, 1158 insertions(+), 68 deletions(-) diff --git a/big_tests/tests/domain_helper.erl b/big_tests/tests/domain_helper.erl index 6cbdfa68f0a..74e056379af 100644 --- a/big_tests/tests/domain_helper.erl +++ b/big_tests/tests/domain_helper.erl @@ -3,7 +3,9 @@ -export([insert_configured_domains/0, delete_configured_domains/0, insert_domain/3, + insert_persistent_domain/3, delete_domain/2, + delete_persistent_domain/3, set_domain_password/3, delete_domain_password/2, make_metrics_prefix/1, diff --git a/big_tests/tests/graphql_account_SUITE.erl b/big_tests/tests/graphql_account_SUITE.erl index 4fb1e907e79..3b55953855a 100644 --- a/big_tests/tests/graphql_account_SUITE.erl +++ b/big_tests/tests/graphql_account_SUITE.erl @@ -6,7 +6,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_command/4, execute_user_command/5, get_listener_port/1, - get_listener_config/1, get_ok_value/2, get_err_msg/1]). + get_listener_config/1, get_ok_value/2, get_err_msg/1, + execute_domain_admin_command/4, get_unauthorized/1]). -define(NOT_EXISTING_JID, <<"unknown987@unknown">>). @@ -16,12 +17,14 @@ suite() -> all() -> [{group, user_account}, {group, admin_account_http}, - {group, admin_account_cli}]. + {group, admin_account_cli}, + {group, domain_admin_account}]. groups() -> [{user_account, [parallel], user_account_tests()}, {admin_account_http, [], admin_account_tests()}, - {admin_account_cli, [], admin_account_tests()}]. + {admin_account_cli, [], admin_account_tests()}, + {domain_admin_account, [], domain_admin_tests()}]. user_account_tests() -> [user_unregister, @@ -41,6 +44,22 @@ admin_account_tests() -> admin_ban_user, admin_change_user_password]. +domain_admin_tests() -> + [domain_admin_list_users, + domain_admin_count_users, + domain_admin_check_password, + domain_admin_check_password_hash, + domain_admin_check_plain_password_hash, + domain_admin_check_user, + admin_register_user, + domain_admin_register_user_no_permission, + admin_register_random_user, + domain_admin_remove_non_existing_user, + admin_remove_existing_user, + domain_admin_remove_user_no_permission, + domain_admin_ban_user, + domain_admin_change_user_password]. + init_per_suite(Config) -> Config1 = [{ctl_auth_mods, mongoose_helper:auth_modules()} | Config], Config2 = escalus:init_per_suite(Config1), @@ -55,12 +74,17 @@ init_per_group(admin_account_http, Config) -> graphql_helper:init_admin_handler(init_users(Config)); init_per_group(admin_account_cli, Config) -> graphql_helper:init_admin_cli(init_users(Config)); +init_per_group(domain_admin_account, Config) -> + graphql_helper:init_domain_admin_handler(domain_admin_init_users(Config)); init_per_group(user_account, Config) -> graphql_helper:init_user(Config). end_per_group(user_account, _Config) -> graphql_helper:clean(), escalus_fresh:clean(); +end_per_group(domain_admin_account, Config) -> + graphql_helper:clean(), + domain_admin_clean_users(Config); end_per_group(_GroupName, Config) -> graphql_helper:clean(), clean_users(Config). @@ -68,10 +92,17 @@ end_per_group(_GroupName, Config) -> init_users(Config) -> escalus:create_users(Config, escalus:get_users([alice])). +domain_admin_init_users(Config) -> + escalus:create_users(Config, escalus:get_users([alice, alice_bis])). + clean_users(Config) -> escalus_fresh:clean(), escalus:delete_users(Config, escalus:get_users([alice])). +domain_admin_clean_users(Config) -> + escalus_fresh:clean(), + escalus:delete_users(Config, escalus:get_users([alice, alice_bis])). + init_per_testcase(admin_register_user = C, Config) -> Config1 = [{user, {<<"gql_admin_registration_test">>, domain_helper:domain()}} | Config], escalus:init_per_testcase(C, Config1); @@ -87,6 +118,21 @@ init_per_testcase(admin_check_plain_password_hash = C, Config) -> Config2 = escalus:create_users(Config1, escalus:get_users([carol])), escalus:init_per_testcase(C, Config2) end; +init_per_testcase(domain_admin_check_plain_password_hash = C, Config) -> + {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config), + case lists:member(ejabberd_auth_ldap, AuthMods) of + true -> + {skip, not_fully_supported_with_ldap}; + false -> + AuthOpts = mongoose_helper:auth_opts_with_password_format(plain), + Config1 = mongoose_helper:backup_and_set_config_option( + Config, {auth, domain_helper:host_type()}, AuthOpts), + Config2 = escalus:create_users(Config1, escalus:get_users([alice_bis])), + escalus:init_per_testcase(C, Config2) + end; +init_per_testcase(domain_admin_register_user = C, Config) -> + Config1 = [{user, {<<"gql_domain_admin_registration_test">>, domain_helper:domain()}} | Config], + escalus:init_per_testcase(C, Config1); init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). @@ -97,6 +143,13 @@ end_per_testcase(admin_register_user = C, Config) -> end_per_testcase(admin_check_plain_password_hash, Config) -> mongoose_helper:restore_config(Config), escalus:delete_users(Config, escalus:get_users([carol])); +end_per_testcase(domain_admin_check_plain_password_hash, Config) -> + mongoose_helper:restore_config(Config), + escalus:delete_users(Config, escalus:get_users([carol, alice_bis])); +end_per_testcase(domain_admin_register_user = C, Config) -> + {Username, Domain} = proplists:get_value(user, Config), + rpc(mim(), mongoose_account_api, unregister_user, [Username, Domain]), + escalus:end_per_testcase(C, Config); end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). @@ -262,6 +315,122 @@ admin_change_user_password(Config) -> ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) end). +domain_admin_list_users(Config) -> + % An unknown domain + Resp = list_users(<<"unknown-domain">>, Config), + get_unauthorized(Resp), + % A domain with users + Domain = domain_helper:domain(), + Username = jid:nameprep(escalus_users:get_username(Config, alice)), + JID = <>, + Resp2 = list_users(Domain, Config), + Users = get_ok_value([data, account, listUsers], Resp2), + ?assert(lists:member(JID, Users)). + +domain_admin_count_users(Config) -> + % An unknown domain + Resp = count_users(<<"unknown-domain">>, Config), + get_unauthorized(Resp), + % A domain with at least one user + Domain = domain_helper:domain(), + Resp2 = count_users(Domain, Config), + ?assert(0 < get_ok_value([data, account, countUsers], Resp2)). + +domain_admin_check_password(Config) -> + Password = lists:last(escalus_users:get_usp(Config, alice)), + BinJID = escalus_users:get_jid(Config, alice), + Path = [data, account, checkPassword], + % A correct password + Resp1 = check_password(BinJID, Password, Config), + ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), + % An incorrect password + Resp2 = check_password(BinJID, <<"incorrect_pw">>, Config), + ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)), + % An external domain + PasswordOutside = lists:last(escalus_users:get_usp(Config, alice_bis)), + BinOutsideJID = escalus_users:get_jid(Config, alice_bis), + Resp3 = check_password(BinOutsideJID, PasswordOutside, Config), + get_unauthorized(Resp3), + % A non-existing user + Resp4 = check_password(?NOT_EXISTING_JID, Password, Config), + get_unauthorized(Resp4). + +domain_admin_check_password_hash(Config) -> + UserSCRAM = escalus_users:get_jid(Config, alice), + ExternalUserSCRAM = escalus_users:get_jid(Config, alice_bis), + EmptyHash = list_to_binary(get_md5(<<>>)), + Method = <<"md5">>, + % SCRAM password user + Resp1 = check_password_hash(UserSCRAM, EmptyHash, Method, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)), + % An external domain user + Resp2 = check_password_hash(ExternalUserSCRAM, EmptyHash, Method, Config), + get_unauthorized(Resp2). + +domain_admin_check_plain_password_hash(Config) -> + Method = <<"md5">>, + ExternalUserJID = escalus_users:get_jid(Config, alice_bis), + ExternalPassword = lists:last(escalus_users:get_usp(Config, alice_bis)), + ExternalHash = list_to_binary(get_md5(ExternalPassword)), + get_unauthorized(check_password_hash(ExternalUserJID, ExternalHash, Method, Config)). + +domain_admin_check_user(Config) -> + BinJID = escalus_users:get_jid(Config, alice), + ExternalBinJID = escalus_users:get_jid(Config, alice_bis), + Path = [data, account, checkUser], + % An existing user + Resp1 = check_user(BinJID, Config), + ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), + % An external domain user + Resp2 = check_user(ExternalBinJID, Config), + get_unauthorized(Resp2). + +domain_admin_register_user_no_permission(Config) -> + Password = <<"my_password">>, + Domain = <<"unknown-domain">>, + get_unauthorized(register_user(Domain, external_user, Password, Config)). + +domain_admin_remove_non_existing_user(Config) -> + get_unauthorized(remove_user(?NOT_EXISTING_JID, Config)). + +domain_admin_remove_user_no_permission(Config) -> + escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> + BinJID = escalus_client:full_jid(AliceBis), + get_unauthorized(remove_user(BinJID, Config)) + end). + +domain_admin_ban_user(Config) -> + Path = [data, account, banUser, message], + Reason = <<"annoying">>, + % Ban not existing user + Resp1 = ban_user(?NOT_EXISTING_JID, Reason, Config), + get_unauthorized(Resp1), + % Ban an existing user + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + BinJID = escalus_client:full_jid(Alice), + Resp2 = ban_user(BinJID, Reason, Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"successfully banned">>)) + end). + +domain_admin_change_user_password(Config) -> + Path = [data, account, changeUserPassword, message], + NewPassword = <<"new password">>, + % Change password of not existing user + Resp1 = change_user_password(?NOT_EXISTING_JID, NewPassword, Config), + get_unauthorized(Resp1), + % Set an empty password + escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) -> + BinJID = escalus_client:full_jid(Alice1), + Resp2 = change_user_password(BinJID, <<>>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"Empty password">>)) + end), + % Change password of an existing user + escalus:fresh_story(Config, [{alice, 1}], fun(Alice2) -> + BinJID = escalus_client:full_jid(Alice2), + Resp3 = change_user_password(BinJID, NewPassword, Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) + end). + %% Helpers get_md5(AccountPass) -> diff --git a/big_tests/tests/graphql_domain_SUITE.erl b/big_tests/tests/graphql_domain_SUITE.erl index c6ef6420d0d..dd27283b466 100644 --- a/big_tests/tests/graphql_domain_SUITE.erl +++ b/big_tests/tests/graphql_domain_SUITE.erl @@ -5,21 +5,27 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, [execute_command/4, get_ok_value/2, get_err_msg/1, skip_null_fields/1]). +-import(graphql_helper, [execute_command/4, get_ok_value/2, get_err_msg/1, skip_null_fields/1, + execute_domain_admin_command/4, get_unauthorized/1]). -define(HOST_TYPE, <<"dummy auth">>). -define(SECOND_HOST_TYPE, <<"test type">>). +-define(EXAMPLE_DOMAIN, <<"example.com">>). +-define(SECOND_EXAMPLE_DOMAIN, <<"second.example.com">>). +-define(DOMAIN_ADMIN_EXAMPLE_DOMAIN, <<"domain-admin.example.com">>). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> [{group, domain_http}, - {group, domain_cli}]. + {group, domain_cli}, + {group, domain_admin_tests}]. groups() -> [{domain_http, [sequence], domain_tests()}, - {domain_cli, [sequence], domain_tests()}]. + {domain_cli, [sequence], domain_tests()}, + {domain_admin_tests, [sequence], domain_admin_tests()}]. domain_tests() -> [create_domain, @@ -41,6 +47,19 @@ domain_tests() -> delete_domain_password ]. +domain_admin_tests() -> + [domain_admin_get_domain_details, + domain_admin_set_domain_password, + domain_admin_create_domain_no_permission, + domain_admin_disable_domain_no_permission, + domain_admin_enable_domain_no_permission, + domain_admin_get_domains_by_host_type_no_permission, + domain_admin_get_domain_details_no_permission, + domain_admin_delete_domain_no_permission, + domain_admin_set_domain_password_no_permission, + domain_admin_delete_domain_password_no_permission + ]. + init_per_suite(Config) -> case mongoose_helper:is_rdbms_enabled(?HOST_TYPE) of true -> @@ -57,8 +76,15 @@ end_per_suite(Config) -> init_per_group(domain_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(domain_cli, Config) -> - graphql_helper:init_admin_cli(Config). - + graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_tests, Config) -> + domain_helper:insert_persistent_domain(mim(), ?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE), + domain_helper:insert_domain(mim(), ?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE), + graphql_helper:init_domain_admin_handler(Config, ?DOMAIN_ADMIN_EXAMPLE_DOMAIN). + +end_per_group(domain_admin_tests, _Config) -> + domain_helper:delete_domain(mim(), ?DOMAIN_ADMIN_EXAMPLE_DOMAIN), + domain_helper:delete_persistent_domain(mim(), ?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE); end_per_group(_GroupName, _Config) -> graphql_helper:clean(). @@ -69,10 +95,10 @@ end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). create_domain(Config) -> - create_domain(Config, <<"exampleDomain">>), - create_domain(Config, <<"exampleDomain2">>). + create_domain(?EXAMPLE_DOMAIN, Config), + create_domain(?SECOND_EXAMPLE_DOMAIN, Config). -create_domain(Config, DomainName) -> +create_domain(DomainName, Config) -> Result = add_domain(DomainName, ?HOST_TYPE, Config), ParsedResult = get_ok_value([data, domain, addDomain], Result), ?assertEqual(#{<<"domain">> => DomainName, @@ -80,7 +106,7 @@ create_domain(Config, DomainName) -> <<"enabled">> => null}, ParsedResult). unknown_host_type_error_formatting(Config) -> - DomainName = <<"exampleDomain">>, + DomainName = ?EXAMPLE_DOMAIN, HostType = <<"NonExistingHostType">>, Result = add_domain(DomainName, HostType, Config), ?assertEqual(<<"Unknown host type">>, get_err_msg(Result)). @@ -91,7 +117,7 @@ static_domain_error_formatting(Config) -> ?assertEqual(<<"Domain static">>, get_err_msg(Result)). domain_duplicate_error_formatting(Config) -> - DomainName = <<"exampleDomain">>, + DomainName = ?EXAMPLE_DOMAIN, Result = add_domain(DomainName, ?SECOND_HOST_TYPE, Config), ?assertEqual(<<"Domain already exists">>, get_err_msg(Result)). @@ -111,44 +137,44 @@ domain_not_found_error_formatting_after_query(Config) -> domain_not_found_error_formatting(Result). wrong_host_type_error_formatting(Config) -> - Result = remove_domain(<<"exampleDomain">>, ?SECOND_HOST_TYPE, Config), + Result = remove_domain(?EXAMPLE_DOMAIN, ?SECOND_HOST_TYPE, Config), ?assertEqual(<<"Wrong host type">>, get_err_msg(Result)). disable_domain(Config) -> - Result = disable_domain(<<"exampleDomain">>, Config), + Result = disable_domain(?EXAMPLE_DOMAIN, Config), ParsedResult = get_ok_value([data, domain, disableDomain], Result), - ?assertMatch(#{<<"domain">> := <<"exampleDomain">>, <<"enabled">> := false}, ParsedResult), - {ok, Domain} = rpc(mim(), mongoose_domain_sql, select_domain, [<<"exampleDomain">>]), + ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"enabled">> := false}, ParsedResult), + {ok, Domain} = rpc(mim(), mongoose_domain_sql, select_domain, [?EXAMPLE_DOMAIN]), ?assertEqual(#{host_type => ?HOST_TYPE, enabled => false}, Domain). enable_domain(Config) -> - Result = enable_domain(<<"exampleDomain">>, Config), + Result = enable_domain(?EXAMPLE_DOMAIN, Config), ParsedResult = get_ok_value([data, domain, enableDomain], Result), - ?assertMatch(#{<<"domain">> := <<"exampleDomain">>, <<"enabled">> := true}, ParsedResult). + ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"enabled">> := true}, ParsedResult). get_domains_by_host_type(Config) -> Result = get_domains_by_host_type(?HOST_TYPE, Config), ParsedResult = get_ok_value([data, domain, domainsByHostType], Result), - ?assertEqual(lists:sort([<<"exampleDomain">>, <<"exampleDomain2">>]), + ?assertEqual(lists:sort([?EXAMPLE_DOMAIN, ?SECOND_EXAMPLE_DOMAIN]), lists:sort(ParsedResult)). get_domain_details(Config) -> - Result = get_domain_details(<<"exampleDomain">>, Config), + Result = get_domain_details(?EXAMPLE_DOMAIN, Config), ParsedResult = get_ok_value([data, domain, domainDetails], Result), - ?assertEqual(#{<<"domain">> => <<"exampleDomain">>, + ?assertEqual(#{<<"domain">> => ?EXAMPLE_DOMAIN, <<"hostType">> => ?HOST_TYPE, <<"enabled">> => true}, ParsedResult). delete_domain(Config) -> - Result1 = remove_domain(<<"exampleDomain">>, ?HOST_TYPE, Config), + Result1 = remove_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config), ParsedResult1 = get_ok_value([data, domain, removeDomain], Result1), ?assertMatch(#{<<"msg">> := <<"Domain removed!">>, - <<"domain">> := #{<<"domain">> := <<"exampleDomain">>}}, + <<"domain">> := #{<<"domain">> := ?EXAMPLE_DOMAIN}}, ParsedResult1), - Result2 = remove_domain(<<"exampleDomain2">>, ?HOST_TYPE, Config), + Result2 = remove_domain(?SECOND_EXAMPLE_DOMAIN, ?HOST_TYPE, Config), ParsedResult2 = get_ok_value([data, domain, removeDomain], Result2), ?assertMatch(#{<<"msg">> := <<"Domain removed!">>, - <<"domain">> := #{<<"domain">> := <<"exampleDomain2">>}}, + <<"domain">> := #{<<"domain">> := ?SECOND_EXAMPLE_DOMAIN}}, ParsedResult2). get_domains_after_deletion(Config) -> @@ -171,6 +197,42 @@ delete_domain_password(Config) -> ParsedResult = get_ok_value([data, domain, deleteDomainPassword], Result), ?assertNotEqual(nomatch, binary:match(ParsedResult, <<"successfully">>)). +domain_admin_get_domain_details(Config) -> + Result = get_domain_details(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config), + ParsedResult = get_ok_value([data, domain, domainDetails], Result), + ?assertEqual(#{<<"domain">> => ?DOMAIN_ADMIN_EXAMPLE_DOMAIN, + <<"hostType">> => ?HOST_TYPE, + <<"enabled">> => true}, ParsedResult). + +domain_admin_set_domain_password(Config) -> + Result = set_domain_password(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, <<"secret">>, Config), + ParsedResult = get_ok_value([data, domain, setDomainPassword], Result), + ?assertNotEqual(nomatch, binary:match(ParsedResult, <<"successfully">>)). + +domain_admin_create_domain_no_permission(Config) -> + get_unauthorized(add_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). + +domain_admin_disable_domain_no_permission(Config) -> + get_unauthorized(disable_domain(?EXAMPLE_DOMAIN, Config)). + +domain_admin_enable_domain_no_permission(Config) -> + get_unauthorized(enable_domain(?EXAMPLE_DOMAIN, Config)). + +domain_admin_get_domains_by_host_type_no_permission(Config) -> + get_unauthorized(get_domains_by_host_type(?HOST_TYPE, Config)). + +domain_admin_get_domain_details_no_permission(Config) -> + get_unauthorized(get_domain_details(?EXAMPLE_DOMAIN, Config)). + +domain_admin_set_domain_password_no_permission(Config) -> + get_unauthorized(set_domain_password(<<"externalDomain">>, <<"secret">>, Config)). + +domain_admin_delete_domain_no_permission(Config) -> + get_unauthorized(remove_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). + +domain_admin_delete_domain_password_no_permission(Config) -> + get_unauthorized(delete_domain_password(domain_helper:domain(), Config)). + %% Commands add_domain(Domain, HostType, Config) -> diff --git a/big_tests/tests/graphql_gdpr_SUITE.erl b/big_tests/tests/graphql_gdpr_SUITE.erl index ce067093297..ae64c5aca47 100644 --- a/big_tests/tests/graphql_gdpr_SUITE.erl +++ b/big_tests/tests/graphql_gdpr_SUITE.erl @@ -5,7 +5,7 @@ -import(domain_helper, [host_type/0, domain/0]). -import(distributed_helper, [mim/0, rpc/4, require_rpc_nodes/1]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, - get_ok_value/2, get_err_code/1]). + get_ok_value/2, get_err_code/1, get_unauthorized/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -15,16 +15,23 @@ suite() -> all() -> [{group, admin_gdpr_http}, - {group, admin_gdpr_cli}]. + {group, admin_gdpr_cli}, + {group, domain_admin_gdpr}]. groups() -> [{admin_gdpr_http, [], admin_stats_tests()}, - {admin_gdpr_cli, [], admin_stats_tests()}]. + {admin_gdpr_cli, [], admin_stats_tests()}, + {domain_admin_gdpr, [], domain_admin_stats_tests()}]. admin_stats_tests() -> [admin_retrive_user_data, admin_gdpr_no_user_test]. +domain_admin_stats_tests() -> + [admin_retrive_user_data, + admin_gdpr_no_user_test, + domain_admin_retrive_user_data_no_permission]. + init_per_suite(Config) -> Config1 = escalus:init_per_suite(Config), ejabberd_node_utils:init(mim(), Config1). @@ -35,7 +42,9 @@ end_per_suite(Config) -> init_per_group(admin_gdpr_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_gdpr_cli, Config) -> - graphql_helper:init_admin_cli(Config). + graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_gdpr, Config) -> + graphql_helper:init_domain_admin_handler(Config). end_per_group(_, _Config) -> graphql_helper:clean(), @@ -68,6 +77,16 @@ admin_gdpr_no_user_test(Config) -> Res = admin_retrieve_personal_data(<<"AAAA">>, domain(), <<"AAA">>, Config), ?assertEqual(<<"user_does_not_exist_error">>, get_err_code(Res)). +domain_admin_retrive_user_data_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_retrive_user_data_no_permission/2). + +domain_admin_retrive_user_data_no_permission(Config, Alice) -> + Filename = random_filename(Config), + Res = admin_retrieve_personal_data(escalus_client:username(Alice), escalus_client:server(Alice), + list_to_binary(Filename), Config), + get_unauthorized(Res). + % Helpers admin_retrieve_personal_data(Username, Domain, ResultFilepath, Config) -> diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index 99b651ec733..13e46768fed 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -100,7 +100,9 @@ init_user(Config) -> add_specs([{schema_endpoint, user} | Config]). init_domain_admin_handler(Config) -> - Domain = domain_helper:domain(), + init_domain_admin_handler(Config, domain_helper:domain()). + +init_domain_admin_handler(Config, Domain) -> case mongoose_helper:is_rdbms_enabled(Domain) of true -> Password = base16:encode(crypto:strong_rand_bytes(8)), @@ -139,9 +141,8 @@ get_err_msg(Resp) -> get_err_msg(1, Resp). get_unauthorized({Code, #{<<"errors">> := Errors}}) -> - [#{<<"extensions">> := #{<<"code">> := ErrorCode}}] = Errors, - assert_response_code(unauthorized, Code), - ?assertEqual(<<"no_permissions">>, ErrorCode). + [#{<<"extensions">> := #{<<"code">> := _ErrorCode}}] = Errors, + assert_response_code(unauthorized, Code). get_coercion_err_msg({Code, #{<<"errors">> := [Error]}}) -> assert_response_code(bad_request, Code), diff --git a/big_tests/tests/graphql_http_upload_SUITE.erl b/big_tests/tests/graphql_http_upload_SUITE.erl index fe1a847c3a9..fbcc11b4ee9 100644 --- a/big_tests/tests/graphql_http_upload_SUITE.erl +++ b/big_tests/tests/graphql_http_upload_SUITE.erl @@ -5,7 +5,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(domain_helper, [host_type/0, domain/0]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, - get_err_msg/1, get_err_code/1]). + get_err_msg/1, get_err_code/1, get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). @@ -17,16 +17,20 @@ suite() -> all() -> [{group, user}, {group, admin_http}, - {group, admin_cli}]. + {group, admin_cli}, + {group, domain_admin}]. groups() -> [{user, [], user_groups()}, {admin_http, [], admin_groups()}, {admin_cli, [], admin_groups()}, + {domain_admin, [], domain_admin_groups()}, {user_http_upload, [], user_http_upload_tests()}, {user_http_upload_not_configured, [], user_http_upload_not_configured_tests()}, {admin_http_upload, [], admin_http_upload_tests()}, - {admin_http_upload_not_configured, [], admin_http_upload_not_configured_tests()}]. + {admin_http_upload_not_configured, [], admin_http_upload_not_configured_tests()}, + {domain_admin_http_upload, [], domain_admin_http_upload_tests()}, + {domain_admin_http_upload_not_configured, [], domain_admin_http_upload_not_configured_tests()}]. user_groups() -> [{group, user_http_upload}, @@ -36,6 +40,10 @@ admin_groups() -> [{group, admin_http_upload}, {group, admin_http_upload_not_configured}]. +domain_admin_groups() -> + [{group, domain_admin_http_upload}, + {group, domain_admin_http_upload_not_configured}]. + user_http_upload_tests() -> [user_get_url_test, user_get_url_zero_size, @@ -55,6 +63,16 @@ admin_http_upload_tests() -> admin_http_upload_not_configured_tests() -> [admin_http_upload_not_configured]. +domain_admin_http_upload_tests() -> + [admin_get_url_test, + admin_get_url_zero_size, + admin_get_url_too_large_size, + admin_get_url_zero_timeout, + domain_admin_get_url_no_permission]. + +domain_admin_http_upload_not_configured_tests() -> + [admin_http_upload_not_configured]. + init_per_suite(Config) -> Config1 = dynamic_modules:save_modules(host_type(), Config), Config2 = ejabberd_node_utils:init(mim(), Config1), @@ -70,6 +88,8 @@ init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_http_upload, Config) -> dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, create_opts(?S3_HOSTNAME, true)}]), @@ -81,7 +101,14 @@ init_per_group(admin_http_upload, Config) -> dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, create_opts(?S3_HOSTNAME, true)}]), Config; +init_per_group(domain_admin_http_upload, Config) -> + dynamic_modules:ensure_modules(host_type(), + [{mod_http_upload, create_opts(?S3_HOSTNAME, true)}]), + Config; init_per_group(admin_http_upload_not_configured, Config) -> + dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, stopped}]), + Config; +init_per_group(domain_admin_http_upload_not_configured, Config) -> dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, stopped}]), Config. @@ -194,6 +221,10 @@ admin_http_upload_not_configured(Config) -> ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)), ?assertEqual(<<"mod_http_upload is not loaded for this host">>, get_err_msg(Result)). +domain_admin_get_url_no_permission(Config) -> + Result = admin_get_url(<<"AAAAA">>, <<"test">>, 123, <<"Test">>, 123, Config), + get_unauthorized(Result). + % Helpers user_get_url(FileName, Size, ContentType, Timeout, User, Config) -> diff --git a/big_tests/tests/graphql_inbox_SUITE.erl b/big_tests/tests/graphql_inbox_SUITE.erl index de4f8332e43..e575075b506 100644 --- a/big_tests/tests/graphql_inbox_SUITE.erl +++ b/big_tests/tests/graphql_inbox_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_user_command/5, execute_command/4, user_to_bin/1, - get_ok_value/2, get_err_msg/1, get_err_code/1]). + get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). -include("inbox.hrl"). @@ -21,12 +21,14 @@ all() -> tests() -> [{group, user_inbox}, {group, admin_inbox_http}, - {group, admin_inbox_cli}]. + {group, admin_inbox_cli}, + {group, domain_admin_inbox}]. groups() -> [{user_inbox, [], user_inbox_tests()}, {admin_inbox_http, [], admin_inbox_tests()}, - {admin_inbox_cli, [], admin_inbox_tests()}]. + {admin_inbox_cli, [], admin_inbox_tests()}, + {domain_admin_inbox, [], domain_admin_inbox_tests()}]. user_inbox_tests() -> [user_flush_own_bin]. @@ -40,6 +42,14 @@ admin_inbox_tests() -> admin_flush_global_bin_after_days, admin_try_flush_nonexistent_host_type_bin]. +domain_admin_inbox_tests() -> + [admin_flush_user_bin, + domain_admin_try_flush_nonexistent_user_bin, + admin_flush_domain_bin, + domain_admin_try_flush_domain_bin_no_permission, + domain_admin_flush_global_bin_no_permission, + domain_admin_try_flush_nonexistent_host_type_bin]. + init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), @@ -58,6 +68,8 @@ init_per_group(admin_inbox_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_inbox_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_inbox, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_inbox, Config) -> graphql_helper:init_user(Config). @@ -146,6 +158,32 @@ admin_try_flush_nonexistent_host_type_bin(Config) -> ?assertErrMsg(Res, <<"not found">>), ?assertErrCode(Res, host_type_not_found). +%% Domain admin test cases + +domain_admin_try_flush_nonexistent_user_bin(Config) -> + %% Check nonexistent domain error + get_unauthorized(flush_user_bin(<<"user@user.com">>, Config)), + %% Check nonexistent user error + User = <<"nonexistent-user@", (domain_helper:domain())/binary>>, + Res2 = flush_user_bin(User, Config), + ?assertErrMsg(Res2, <<"does not exist">>), + ?assertErrCode(Res2, user_does_not_exist). + +domain_admin_try_flush_domain_bin_no_permission(Config) -> + get_unauthorized(flush_domain_bin(<<"external-domain">>, Config)). + +domain_admin_flush_global_bin_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}], + fun domain_admin_flush_global_bin_no_permission/4). + +domain_admin_flush_global_bin_no_permission(Config, Alice, AliceBis, Kate) -> + SecHostType = domain_helper:host_type(), + create_room_and_make_users_leave(Alice, AliceBis, Kate), + get_unauthorized(flush_global_bin(SecHostType, null, Config)). + +domain_admin_try_flush_nonexistent_host_type_bin(Config) -> + get_unauthorized(flush_global_bin(<<"nonexistent host type">>, null, Config)). + %% User test cases user_flush_own_bin(Config) -> diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 928617daa2a..2f842272227 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, user_to_jid/1, - get_ok_value/2, get_err_msg/1, get_err_code/1]). + get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). @@ -20,19 +20,27 @@ suite() -> all() -> [{group, user}, {group, admin_http}, - {group, admin_cli}]. + {group, admin_cli}, + {group, domain_admin}]. groups() -> [{user, [parallel], user_tests()}, {admin_http, [], admin_groups()}, {admin_cli, [], admin_groups()}, + {domain_admin, [], domain_admin_groups()}, {admin_last, [], admin_last_tests()}, - {admin_old_users, [], admin_old_users_tests()}]. + {domain_admin_last, [], domain_admin_last_tests()}, + {admin_old_users, [], admin_old_users_tests()}, + {domain_admin_old_users, [], domain_admin_old_users_tests()}]. admin_groups() -> [{group, admin_last}, {group, admin_old_users}]. +domain_admin_groups() -> + [{group, domain_admin_last}, + {group, domain_admin_old_users}]. + user_tests() -> [user_set_last, user_get_last, @@ -57,6 +65,25 @@ admin_old_users_tests() -> admin_user_without_last_info_is_old_user, admin_logged_user_is_not_old_user]. +domain_admin_last_tests() -> + [admin_set_last, + domain_admin_try_set_nonexistent_user_last, + admin_get_last, + domain_admin_get_nonexistent_user_last, + admin_try_get_nonexistent_last, + admin_count_active_users, + domain_admin_try_count_external_domain_active_users]. + +domain_admin_old_users_tests() -> + [admin_list_old_users_domain, + domain_admin_try_list_old_users_external_domain, + domain_admin_list_old_users_global, + domain_admin_remove_old_users_global, + domain_admin_try_remove_old_users_external_domain, + domain_admin_remove_old_users_global, + domain_admin_user_without_last_info_is_old_user, + domain_admin_logged_user_is_not_old_user]. + init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), @@ -79,9 +106,19 @@ init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(admin_last, Config) -> Config; +init_per_group(domain_admin_last, Config) -> + Config; init_per_group(admin_old_users, Config) -> + AuthMods = mongoose_helper:auth_modules(), + case lists:member(ejabberd_auth_ldap, AuthMods) of + true -> {skip, not_fully_supported_with_ldap}; + false -> Config + end; +init_per_group(domain_admin_old_users, Config) -> AuthMods = mongoose_helper:auth_modules(), case lists:member(ejabberd_auth_ldap, AuthMods) of true -> {skip, not_fully_supported_with_ldap}; @@ -101,7 +138,10 @@ init_per_testcase(C, Config) when C =:= admin_remove_old_users_domain; C =:= admin_remove_old_users_global; C =:= admin_list_old_users_domain; C =:= admin_list_old_users_global; - C =:= admin_user_without_last_info_is_old_user -> + C =:= admin_user_without_last_info_is_old_user; + C =:= domain_admin_list_old_users_global; + C =:= domain_admin_remove_old_users_global; + C =:= domain_admin_user_without_last_info_is_old_user -> Config1 = escalus:create_users(Config, escalus:get_users([alice, bob, alice_bis])), escalus:init_per_testcase(C, Config1); init_per_testcase(CaseName, Config) -> @@ -111,7 +151,10 @@ end_per_testcase(C, Config) when C =:= admin_remove_old_users_domain; C =:= admin_remove_old_users_global; C =:= admin_list_old_users_domain; C =:= admin_list_old_users_global; - C =:= admin_user_without_last_info_is_old_user -> + C =:= admin_user_without_last_info_is_old_user; + C =:= domain_admin_list_old_users_global; + C =:= domain_admin_remove_old_users_global; + C =:= domain_admin_user_without_last_info_is_old_user-> escalus:delete_users(Config, escalus:get_users([alice, bob, alice_bis])), escalus:end_per_testcase(C, Config); end_per_testcase(CaseName, Config) -> @@ -290,6 +333,61 @@ admin_logged_user_is_not_old_user_story(Config, _Alice) -> Res = admin_list_old_users(null, now_dt_with_offset(100), Config), ?assertEqual([], get_ok_value(p(listOldUsers), Res)). +%% Domain admin test cases + +domain_admin_try_set_nonexistent_user_last(Config) -> + get_unauthorized(admin_set_last(?NONEXISTENT_JID, <<"status">>, null, Config)). + +domain_admin_get_nonexistent_user_last(Config) -> + get_unauthorized(admin_get_last(?NONEXISTENT_JID, Config)). + +domain_admin_try_count_external_domain_active_users(Config) -> + get_unauthorized(admin_count_active_users(<<"external-domain.com">>, null, Config)). + +%% Domain admin old users test cases + +domain_admin_try_list_old_users_external_domain(Config) -> + get_unauthorized(admin_list_old_users(<<"external">>, now_dt_with_offset(0), Config)). + +domain_admin_try_remove_old_users_external_domain(Config) -> + get_unauthorized(admin_remove_old_users(<<"external">>, now_dt_with_offset(0), Config)). + +domain_admin_list_old_users_global(Config) -> + jids_with_config(Config, [alice, alice_bis, bob], + fun domain_admin_list_old_users_global_story/4). + +domain_admin_list_old_users_global_story(Config, Alice, AliceBis, Bob) -> + OldDateTime = now_dt_with_offset(100), + + set_last(Bob, OldDateTime, Config), + set_last(AliceBis, OldDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + get_unauthorized(admin_list_old_users(null, now_dt_with_offset(150), Config)). + +domain_admin_remove_old_users_global(Config) -> + jids_with_config(Config, [alice, alice_bis, bob], + fun domain_admin_remove_old_users_global_story/4). + +domain_admin_remove_old_users_global_story(Config, Alice, AliceBis, Bob) -> + ToRemoveDateTime = now_dt_with_offset(100), + + set_last(Bob, ToRemoveDateTime, Config), + set_last(AliceBis, ToRemoveDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + get_unauthorized(admin_remove_old_users(null, now_dt_with_offset(150), Config)). + +domain_admin_user_without_last_info_is_old_user(Config) -> + get_unauthorized(admin_list_old_users(null, now_dt_with_offset(150), Config)). + +domain_admin_logged_user_is_not_old_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_logged_user_is_not_old_user_story/2). + +domain_admin_logged_user_is_not_old_user_story(Config, _Alice) -> + get_unauthorized(admin_list_old_users(null, now_dt_with_offset(100), Config)). + %% User test cases user_set_last(Config) -> diff --git a/big_tests/tests/graphql_metric_SUITE.erl b/big_tests/tests/graphql_metric_SUITE.erl index 2c9b6d203c7..a144d158ba7 100644 --- a/big_tests/tests/graphql_metric_SUITE.erl +++ b/big_tests/tests/graphql_metric_SUITE.erl @@ -5,7 +5,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, [execute_command/4, get_ok_value/2]). +-import(graphql_helper, [execute_command/4, get_ok_value/2, get_unauthorized/1]). suite() -> MIM2NodeName = maps:get(node, distributed_helper:mim2()), @@ -15,11 +15,13 @@ suite() -> all() -> [{group, metrics_http}, - {group, metrics_cli}]. + {group, metrics_cli}, + {group, domain_admin_metrics}]. groups() -> [{metrics_http, [], metrics_tests()}, - {metrics_cli, [], metrics_tests()}]. + {metrics_cli, [], metrics_tests()}, + {domain_admin_metrics, [], domain_admin_metrics_tests()}]. metrics_tests() -> [get_all_metrics, @@ -35,6 +37,15 @@ metrics_tests() -> get_by_name_cluster_metrics_as_dicts, get_mim2_cluster_metrics]. +domain_admin_metrics_tests() -> + [domain_admin_get_metrics, + domain_admin_get_metrics_as_dicts, + domain_admin_get_metrics_as_dicts_by_name, + domain_admin_get_metrics_as_dicts_with_keys, + domain_admin_get_cluster_metrics_as_dicts, + domain_admin_get_cluster_metrics_as_dicts_by_name, + domain_admin_get_cluster_metrics_as_dicts_for_nodes]. + init_per_suite(Config) -> Config1 = ejabberd_node_utils:init(mim(), Config), escalus:init_per_suite(Config1). @@ -46,7 +57,9 @@ end_per_suite(Config) -> init_per_group(metrics_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(metrics_cli, Config) -> - graphql_helper:init_admin_cli(Config). + graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_metrics, Config) -> + graphql_helper:init_domain_admin_handler(Config). end_per_group(_GroupName, _Config) -> graphql_helper:clean(). @@ -228,6 +241,30 @@ kv_objects_to_map(List) -> KV = [{Key, Value} || #{<<"key">> := Key, <<"value">> := Value} <- List], maps:from_list(KV). +%% Domain admin test cases + +domain_admin_get_metrics(Config) -> + get_unauthorized(get_metrics(Config)). + +domain_admin_get_metrics_as_dicts(Config) -> + get_unauthorized(get_metrics_as_dicts(Config)). + +domain_admin_get_metrics_as_dicts_by_name(Config) -> + get_unauthorized(get_metrics_as_dicts_by_name([<<"_">>], Config)). + +domain_admin_get_metrics_as_dicts_with_keys(Config) -> + get_unauthorized(get_metrics_as_dicts_with_keys([<<"one">>], Config)). + +domain_admin_get_cluster_metrics_as_dicts(Config) -> + get_unauthorized(get_cluster_metrics_as_dicts(Config)). + +domain_admin_get_cluster_metrics_as_dicts_by_name(Config) -> + get_unauthorized(get_cluster_metrics_as_dicts_by_name([<<"_">>], Config)). + +domain_admin_get_cluster_metrics_as_dicts_for_nodes(Config) -> + Node = atom_to_binary(maps:get(node, distributed_helper:mim2())), + get_unauthorized(get_cluster_metrics_as_dicts_for_nodes([Node], Config)). + %% Admin commands get_metrics(Config) -> diff --git a/big_tests/tests/graphql_muc_SUITE.erl b/big_tests/tests/graphql_muc_SUITE.erl index a1da7eb80ab..bae74e2e398 100644 --- a/big_tests/tests/graphql_muc_SUITE.erl +++ b/big_tests/tests/graphql_muc_SUITE.erl @@ -4,7 +4,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_command/4, execute_user_command/5, get_ok_value/2, get_err_msg/1, - get_coercion_err_msg/1, user_to_bin/1, user_to_full_bin/1, user_to_jid/1]). + get_coercion_err_msg/1, user_to_bin/1, user_to_full_bin/1, user_to_jid/1, + get_unauthorized/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -16,12 +17,14 @@ suite() -> all() -> [{group, user_muc}, {group, admin_muc_http}, - {group, admin_muc_cli}]. + {group, admin_muc_cli}, + {group, domain_admin_muc}]. groups() -> [{user_muc, [parallel], user_muc_tests()}, {admin_muc_http, [parallel], admin_muc_tests()}, - {admin_muc_cli, [], admin_muc_tests()}]. + {admin_muc_cli, [], admin_muc_tests()}, + {domain_admin_muc, [], domain_admin_muc_tests()}]. user_muc_tests() -> [user_create_and_delete_room, @@ -101,6 +104,44 @@ admin_muc_tests() -> admin_try_list_nonexistent_room_affiliations ]. +domain_admin_muc_tests() -> + [admin_create_and_delete_room, + admin_try_create_instant_room_with_nonexistent_domain, + domain_admin_try_create_instant_room_with_nonexistent_user, + admin_try_delete_nonexistent_room, + domain_admin_try_delete_room_with_nonexistent_domain, + admin_list_rooms, + admin_list_room_users, + domain_admin_try_list_users_from_nonexistent_room, + admin_change_room_config, + domain_admin_try_change_nonexistent_room_config, + admin_get_room_config, + domain_admin_try_get_nonexistent_room_config, + admin_invite_user, + admin_invite_user_with_password, + admin_try_invite_user_to_nonexistent_room, + admin_kick_user, + domain_admin_try_kick_user_from_nonexistent_room, + admin_try_kick_user_from_room_without_moderators, + admin_send_message_to_room, + admin_send_private_message, + admin_get_room_messages, + domain_admin_try_get_nonexistent_room_messages, + admin_set_user_affiliation, + domain_admin_try_set_nonexistent_room_user_affiliation, + admin_set_user_role, + domain_admin_try_set_nonexistent_room_user_role, + admin_try_set_nonexistent_nick_role, + admin_try_set_user_role_in_room_without_moderators, + admin_make_user_enter_room, + admin_make_user_enter_room_with_password, + admin_make_user_enter_room_bare_jid, + admin_make_user_exit_room, + admin_make_user_exit_room_bare_jid, + admin_list_room_affiliations, + domain_admin_try_list_nonexistent_room_affiliations + ]. + init_per_suite(Config) -> HostType = domain_helper:host_type(), Config2 = escalus:init_per_suite(Config), @@ -124,6 +165,8 @@ init_per_group(admin_muc_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_muc_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_muc, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_muc, Config) -> graphql_helper:init_user(Config). @@ -582,6 +625,55 @@ admin_try_list_nonexistent_room_affiliations(Config) -> Res = list_room_affiliations(?NONEXISTENT_ROOM, null, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). +%% Domain admin test cases + +domain_admin_try_delete_room_with_nonexistent_domain(Config) -> + RoomJID = jid:make_bare(<<"unknown">>, <<"unknown">>), + get_unauthorized(delete_room(RoomJID, null, Config)). + +domain_admin_try_create_instant_room_with_nonexistent_user(Config) -> + Name = rand_name(), + LocalDomain = domain_helper:domain(), + ExternalDomain = <<"external">>, + MUCServer = muc_helper:muc_host(), + + LocalJID = <<(rand_name())/binary, "@", LocalDomain/binary>>, + Res1 = create_instant_room(MUCServer, Name, LocalJID, <<"Ali">>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res1), <<"not found">>)), + + ExternalJID = <<(rand_name())/binary, "@", ExternalDomain/binary>>, + Res2 = create_instant_room(MUCServer, Name, ExternalJID, <<"Ali">>, Config), + get_unauthorized(Res2). + +domain_admin_try_list_users_from_nonexistent_room(Config) -> + get_unauthorized(list_room_users(?NONEXISTENT_ROOM, Config)). + +domain_admin_try_change_nonexistent_room_config(Config) -> + RoomConfig = #{title => <<"NewTitle">>}, + get_unauthorized(change_room_config(?NONEXISTENT_ROOM, RoomConfig, Config)). + +domain_admin_try_get_nonexistent_room_config(Config) -> + get_unauthorized(get_room_config(?NONEXISTENT_ROOM, Config)). + +domain_admin_try_kick_user_from_nonexistent_room(Config) -> + get_unauthorized(kick_user(?NONEXISTENT_ROOM, <<"ali">>, null, Config)). + +domain_admin_try_get_nonexistent_room_messages(Config) -> + get_unauthorized(get_room_messages(?NONEXISTENT_ROOM, null, null, Config)). + +domain_admin_try_set_nonexistent_room_user_affiliation(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_try_set_nonexistent_room_user_affiliation/2). + +domain_admin_try_set_nonexistent_room_user_affiliation(Config, Alice) -> + get_unauthorized(set_user_affiliation(?NONEXISTENT_ROOM, Alice, admin, Config)). + +domain_admin_try_set_nonexistent_room_user_role(Config) -> + get_unauthorized(set_user_role(?NONEXISTENT_ROOM, <<"Alice">>, moderator, Config)). + +domain_admin_try_list_nonexistent_room_affiliations(Config) -> + get_unauthorized(list_room_affiliations(?NONEXISTENT_ROOM, null, Config)). + %% User test cases user_list_rooms(Config) -> diff --git a/big_tests/tests/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl index 88820592809..d6d8bd562c2 100644 --- a/big_tests/tests/graphql_muc_light_SUITE.erl +++ b/big_tests/tests/graphql_muc_light_SUITE.erl @@ -5,7 +5,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_listener_port/1, get_listener_config/1, get_ok_value/2, get_err_msg/1, - get_coercion_err_msg/1, make_creds/1]). + get_coercion_err_msg/1, make_creds/1, get_unauthorized/1]). -import(config_parser_helper, [mod_config/2]). @@ -38,12 +38,14 @@ suite() -> all() -> [{group, user_muc_light}, {group, admin_muc_light_http}, - {group, admin_muc_light_cli}]. + {group, admin_muc_light_cli}, + {group, domain_admin_muc_light}]. groups() -> [{user_muc_light, [parallel], user_muc_light_tests()}, {admin_muc_light_http, [parallel], admin_muc_light_tests()}, - {admin_muc_light_cli, [], admin_muc_light_tests()}]. + {admin_muc_light_cli, [], admin_muc_light_tests()}, + {domain_admin_muc_light, [], domain_admin_muc_light_tests()}]. user_muc_light_tests() -> [user_create_room, @@ -84,6 +86,24 @@ admin_muc_light_tests() -> admin_blocking_list ]. +domain_admin_muc_light_tests() -> + [admin_create_room, + domain_admin_create_identified_room, + admin_change_room_config, + domain_admin_change_room_config_errors, + admin_invite_user, + admin_invite_user_errors, + domain_admin_delete_room, + admin_kick_user, + admin_send_message_to_room, + admin_send_message_to_room_errors, + domain_admin_get_room_messages, + domain_admin_list_user_rooms, + domain_admin_list_room_users, + domain_admin_get_room_config, + domain_admin_blocking_list + ]. + init_per_suite(Config) -> Config1 = init_modules(Config), Config2 = ejabberd_node_utils:init(mim(), Config1), @@ -119,6 +139,8 @@ init_per_group(admin_muc_light_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_muc_light_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_muc_light, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_muc_light, Config) -> graphql_helper:init_user(Config). @@ -537,6 +559,214 @@ user_blocking_list_story(Config, Alice, Bob) -> <<"entity">> => RoomBin}], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)). +%% Domain admin test cases + +domain_admin_create_identified_room(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_create_identified_room_story/2). + +domain_admin_create_identified_room_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MucServer = ?config(muc_light_host, Config), + Name = <<"domain admin room">>, + Subject = <<"testing">>, + Id = <<"domain_admin_room_", (atom_to_binary(?config(protocol, Config)))/binary>>, + Res = create_room(MucServer, Name, AliceBin, Subject, Id, Config), + #{<<"jid">> := JID, <<"name">> := Name, <<"subject">> := Subject} = + get_ok_value(?CREATE_ROOM_PATH, Res), + ?assertMatch(#jid{luser = Id, lserver = MucServer}, jid:from_binary(JID)), + % Create a room with an existing ID + Res2 = create_room(MucServer, <<"snd room">>, AliceBin, Subject, Id, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"already exists">>)), + % Try with a non-existent domain + Res3 = create_room(?UNKNOWN_DOMAIN, <<"name">>, AliceBin, Subject, Id, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)), + % Try with an empty string passed as ID + Res4 = create_room(MucServer, <<"name">>, AliceBin, Subject, <<>>, Config), + ?assertNotEqual(nomatch, binary:match(get_coercion_err_msg(Res4), <<"Given string is empty">>)). + +domain_admin_change_room_config_errors(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_change_room_config_errors_story/3). + +domain_admin_change_room_config_errors_story(Config, Alice, Bob) -> + AliceBin = escalus_client:short_jid(Alice), + BobBin = escalus_client:short_jid(Bob), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + {ok, _} = invite_user(RoomJID, AliceBin, BobBin), + % Try to change the config with a non-existent domain + Res = change_room_configuration( + make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), + get_unauthorized(Res), + % Try to change the config of the non-existent room + Res2 = change_room_configuration( + make_bare_jid(<<"unknown">>, MUCServer), AliceBin, RoomName, <<"subject2">>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), + % Try to change the config by the non-existent user + Res3 = change_room_configuration( + jid:to_binary(RoomJID), <<"wrong-user@wrong-domain">>, RoomName, <<"subject2">>, + Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not occupy this room">>)), + % Try to change a config by the user without permission + Res4 = change_room_configuration( + jid:to_binary(RoomJID), BobBin, RoomName, <<"subject2">>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res4), + <<"does not have permission to change">>)). + +domain_admin_delete_room(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_delete_room_story/2). + +domain_admin_delete_room_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(muc_light_host, Config), + Name = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, Name, <<"subject">>, AliceBin), + Res = delete_room(jid:to_binary(RoomJID), Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(?DELETE_ROOM_PATH, Res), + <<"successfully">>)), + ?assertEqual({error, not_exists}, get_room_info(jid:from_binary(RoomJID))), + % Try with a non-existent domain + Res2 = delete_room(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), + get_unauthorized(Res2), + % Try with a non-existent room + Res3 = delete_room(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"Cannot remove">>)). + +domain_admin_get_room_messages(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_get_room_messages_story/2). + +domain_admin_get_room_messages_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + %Domain = escalus_client:server(Alice), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + RoomName2 = <<"second room">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + {ok, _} = create_room(MUCServer, RoomName2, <<"subject">>, AliceBin), + Message = <<"Hello friends">>, + send_message_to_room(RoomJID, jid:from_binary(AliceBin), Message), + mam_helper:maybe_wait_for_archive(Config), + % Get messages so far + Limit = 40, + Res = get_room_messages(jid:to_binary(RoomJID), Limit, null, Config), + #{<<"stanzas">> := [#{<<"stanza">> := StanzaXML}], <<"limit">> := Limit} = + get_ok_value(?GET_MESSAGES_PATH, Res), + ?assertMatch({ok, #xmlel{name = <<"message">>}}, exml:parse(StanzaXML)), + % Get messages before the given date and time + Before = <<"2022-02-17T04:54:13+00:00">>, + Res2 = get_room_messages(jid:to_binary(RoomJID), null, Before, Config), + ?assertMatch(#{<<"stanzas">> := [], <<"limit">> := 50}, get_ok_value(?GET_MESSAGES_PATH, Res2)), + % Try to pass too big page size value + Res3 = get_room_messages(jid:to_binary(RoomJID), 51, Before, Config), + ?assertMatch(#{<<"limit">> := 50},get_ok_value(?GET_MESSAGES_PATH, Res3)), + % Try with a non-existent domain + Res4 = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), + get_unauthorized(Res4). + +domain_admin_list_user_rooms(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_list_user_rooms_story/2). + +domain_admin_list_user_rooms_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Domain = escalus_client:server(Alice), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + RoomName2 = <<"second room">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + {ok, #{jid := RoomJID2}} = create_room(MUCServer, RoomName2, <<"subject">>, AliceBin), + Res = list_user_rooms(AliceBin, Config), + ?assertEqual(lists:sort([jid:to_binary(RoomJID), jid:to_binary(RoomJID2)]), + lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res))), + % Try with a non-existent user + Res2 = list_user_rooms(<<"not-exist@", Domain/binary>>, Config), + ?assertEqual([], lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res2))), + % Try with a non-existent domain + Res3 = list_user_rooms(<<"not-exist@not-exist">>, Config), + get_unauthorized(Res3). + +domain_admin_list_room_users(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_list_room_users_story/2). + +domain_admin_list_room_users_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + AliceLower = escalus_utils:jid_to_lower(AliceBin), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + Res = list_room_users(jid:to_binary(RoomJID), Config), + ?assertEqual([#{<<"jid">> => AliceLower, <<"affiliation">> => <<"OWNER">>}], + get_ok_value(?LIST_ROOM_USERS_PATH, Res)), + % Try with a non-existent domain + Res2 = list_room_users(make_bare_jid(RoomJID#jid.luser, ?UNKNOWN_DOMAIN), Config), + get_unauthorized(Res2), + % Try with a non-existent room + Res3 = list_room_users(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + +domain_admin_get_room_config(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_get_room_config_story/2). + +domain_admin_get_room_config_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + AliceLower = escalus_utils:jid_to_lower(AliceBin), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + RoomSubject = <<"Room about nothing">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, RoomSubject, AliceBin), + RoomJIDBin = jid:to_binary(RoomJID), + Res = get_room_config(jid:to_binary(RoomJID), Config), + ?assertEqual(#{<<"jid">> => RoomJIDBin, <<"subject">> => RoomSubject, <<"name">> => RoomName, + <<"options">> => [#{<<"key">> => <<"background">>, <<"value">> => <<>>}, + #{<<"key">> => <<"music">>, <<"value">> => <<>>}, + #{<<"key">> => <<"roomname">>, <<"value">> => RoomName}, + #{<<"key">> => <<"subject">>, <<"value">> => RoomSubject}], + <<"participants">> => [#{<<"jid">> => AliceLower, + <<"affiliation">> => <<"OWNER">>}]}, + get_ok_value([data, muc_light, getRoomConfig], Res)), + % Try with a non-existent domain + Res2 = get_room_config(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), + get_unauthorized(Res2), + % Try with a non-existent room + Res3 = get_room_config(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + +domain_admin_blocking_list(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_blocking_list_story/3). + +domain_admin_blocking_list_story(Config, Alice, Bob) -> + AliceBin = escalus_client:full_jid(Alice), + BobBin = escalus_client:full_jid(Bob), + BobShortBin = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), + Res = get_user_blocking(AliceBin, Config), + ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res)), + Res2 = set_blocking(AliceBin, [{<<"USER">>, <<"DENY">>, BobBin}], Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(?SET_BLOCKING_LIST_PATH, Res2), + <<"successfully">>)), + Res3 = get_user_blocking(AliceBin, Config), + ?assertEqual([#{<<"entityType">> => <<"USER">>, + <<"action">> => <<"DENY">>, + <<"entity">> => BobShortBin}], + get_ok_value(?GET_BLOCKING_LIST_PATH, Res3)), + Res4 = set_blocking(AliceBin, [{<<"USER">>, <<"ALLOW">>, BobBin}], Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(?SET_BLOCKING_LIST_PATH, Res4), + <<"successfully">>)), + Res5 = get_user_blocking(AliceBin, Config), + ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)), + % Check whether errors are handled correctly + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), + Res6 = get_user_blocking(InvalidUser, Config), + get_unauthorized(Res6), + Res7 = set_blocking(InvalidUser, [], Config), + get_unauthorized(Res7). + %% Admin test cases admin_blocking_list(Config) -> diff --git a/big_tests/tests/graphql_offline_SUITE.erl b/big_tests/tests/graphql_offline_SUITE.erl index 040bd94ef9c..ee8cc2e8395 100644 --- a/big_tests/tests/graphql_offline_SUITE.erl +++ b/big_tests/tests/graphql_offline_SUITE.erl @@ -4,7 +4,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(domain_helper, [host_type/0, domain/0]). --import(graphql_helper, [execute_command/4, get_ok_value/2, get_err_code/1, user_to_bin/1]). +-import(graphql_helper, [execute_command/4, get_ok_value/2, get_err_code/1, user_to_bin/1, + get_unauthorized/1]). -import(config_parser_helper, [mod_config/2]). -import(mongooseimctl_helper, [mongooseimctl/3, rpc_call/3]). @@ -17,18 +18,26 @@ suite() -> all() -> [{group, admin_http}, - {group, admin_cli}]. + {group, admin_cli}, + {group, domain_admin}]. groups() -> [{admin_http, [], admin_groups()}, {admin_cli, [], admin_groups()}, + {domain_admin, [], domain_admin_groups()}, {admin_offline, [], admin_offline_tests()}, - {admin_offline_not_configured, [], admin_offline_not_configured_tests()}]. + {admin_offline_not_configured, [], admin_offline_not_configured_tests()}, + {domain_admin_offline, [], domain_admin_offline_tests()}, + {domain_admin_offline_not_configured, [], domain_admin_offline_not_configured_tests()}]. admin_groups() -> [{group, admin_offline}, {group, admin_offline_not_configured}]. +domain_admin_groups() -> + [{group, domain_admin_offline}, + {group, domain_admin_offline_not_configured}]. + admin_offline_tests() -> [admin_delete_expired_messages_test, admin_delete_old_messages_test, @@ -41,6 +50,18 @@ admin_offline_not_configured_tests() -> [admin_delete_expired_messages_offline_not_configured_test, admin_delete_old_messages_offline_not_configured_test]. +domain_admin_offline_tests() -> + [admin_delete_expired_messages_test, + admin_delete_old_messages_test, + admin_delete_expired_messages2_test, + admin_delete_old_messages2_test, + domain_admin_delete_expired_messages_no_domain_test, + domain_admin_delete_old_messages_no_domain_test]. + +domain_admin_offline_not_configured_tests() -> + [admin_delete_expired_messages_offline_not_configured_test, + admin_delete_old_messages_offline_not_configured_test]. + init_per_suite(Config) -> Config1 = dynamic_modules:save_modules(host_type(), Config), Config2 = ejabberd_node_utils:init(mim(), Config1), @@ -61,18 +82,30 @@ init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(admin_offline, Config) -> HostType = host_type(), Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), ModConfig = create_config(Backend), dynamic_modules:ensure_modules(HostType, ModConfig), [{backend, Backend} | escalus:init_per_suite(Config)]; +init_per_group(domain_admin_offline, Config) -> + HostType = host_type(), + Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), + ModConfig = create_config(Backend), + dynamic_modules:ensure_modules(HostType, ModConfig), + [{backend, Backend} | escalus:init_per_suite(Config)]; init_per_group(admin_offline_not_configured, Config) -> + dynamic_modules:ensure_modules(host_type(), [{mod_offline, stopped}]), + Config; +init_per_group(domain_admin_offline_not_configured, Config) -> dynamic_modules:ensure_modules(host_type(), [{mod_offline, stopped}]), Config. end_per_group(GroupName, _Config) when GroupName =:= admin_http; - GroupName =:= admin_cli -> + GroupName =:= admin_cli; + GroupName =:= domain_admin -> graphql_helper:clean(); end_per_group(_, _Config) -> ok. @@ -135,6 +168,14 @@ admin_delete_old_messages_offline_not_configured_test(Config) -> Result = delete_old_messages(domain(), 2, Config), ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)). +%% Domain admin test cases + +domain_admin_delete_expired_messages_no_domain_test(Config) -> + get_unauthorized(delete_expired_messages(<<"AAAA">>, Config)). + +domain_admin_delete_old_messages_no_domain_test(Config) -> + get_unauthorized(delete_old_messages(<<"AAAA">>, 2, Config)). + %% Commands delete_expired_messages(Domain, Config) -> diff --git a/big_tests/tests/graphql_private_SUITE.erl b/big_tests/tests/graphql_private_SUITE.erl index f66aea31be2..8aeb2a4552c 100644 --- a/big_tests/tests/graphql_private_SUITE.erl +++ b/big_tests/tests/graphql_private_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, get_err_code/1, - user_to_bin/1]). + user_to_bin/1, get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("exml/include/exml.hrl"). @@ -13,10 +13,14 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_private}, {group, admin_private_http}, {group, admin_private_cli}]. + [{group, user_private}, + {group, domain_admin_private}, + {group, admin_private_http}, + {group, admin_private_cli}]. groups() -> [{user_private, [], user_private_tests()}, + {domain_admin_private, [], domain_admin_private_tests()}, {admin_private_http, [], admin_private_tests()}, {admin_private_cli, [], admin_private_tests()}]. @@ -25,6 +29,12 @@ user_private_tests() -> user_get_private, parse_xml_error]. +domain_admin_private_tests() -> + [admin_set_private, + admin_get_private, + domain_admin_no_user_error_set, + domain_admin_no_user_error_get]. + admin_private_tests() -> [admin_set_private, admin_get_private, @@ -55,6 +65,8 @@ init_per_group(admin_private_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_private_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_private, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_private, Config) -> graphql_helper:init_user(Config). @@ -143,6 +155,17 @@ private_input() -> attrs = [{<<"xmlns">>, "alice:private:ns"}], children = [{xmlcdata, <<"DATA">>}]}. +%% Domain admin tests + +domain_admin_no_user_error_set(Config) -> + ElemStr = exml:to_binary(private_input()), + Result = admin_set_private(<<"AAAAA">>, ElemStr, Config), + get_unauthorized(Result). + +domain_admin_no_user_error_get(Config) -> + Result = admin_get_private(<<"AAAAA">>, <<"my_element">>, <<"alice:private:ns">>, Config), + get_unauthorized(Result). + %% Commands user_set_private(User, ElementString, Config) -> diff --git a/big_tests/tests/graphql_session_SUITE.erl b/big_tests/tests/graphql_session_SUITE.erl index de864e574b8..28f03efc05b 100644 --- a/big_tests/tests/graphql_session_SUITE.erl +++ b/big_tests/tests/graphql_session_SUITE.erl @@ -6,20 +6,22 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_listener_port/1, - get_listener_config/1, get_ok_value/2, get_err_msg/1]). + get_listener_config/1, get_ok_value/2, get_err_msg/1, get_unauthorized/1]). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> [{group, user_session}, - {group, admin_session}]. + {group, admin_session}, + {group, domain_admin_session}]. groups() -> [{user_session, [parallel], user_session_tests()}, {admin_session, [], [{group, admin_session_http}, {group, admin_session_cli}]}, {admin_session_http, [], admin_session_tests()}, - {admin_session_cli, [], admin_session_tests()}]. + {admin_session_cli, [], admin_session_tests()}, + {domain_admin_session, [], domain_admin_session_tests()}]. user_session_tests() -> [user_list_resources, @@ -39,6 +41,19 @@ admin_session_tests() -> admin_set_presence_away, admin_set_presence_unavailable]. +domain_admin_session_tests() -> + [domain_admin_list_sessions, + domain_admin_count_sessions, + admin_list_user_sessions, + admin_count_user_resources, + admin_get_user_resource, + domain_admin_list_users_with_status, + domain_admin_count_users_with_status, + admin_kick_session, + admin_set_presence, + admin_set_presence_away, + admin_set_presence_unavailable]. + init_per_suite(Config) -> Config1 = ejabberd_node_utils:init(mim(), Config), Config2 = escalus:init_per_suite(Config1), @@ -54,11 +69,23 @@ init_per_group(admin_session_cli, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_session_http, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_session, Config) -> + Config1 = graphql_helper:init_domain_admin_handler(Config), + case Config1 of + {skip, require_rdbms} -> + Config1; + _ -> + escalus:create_users(Config1, escalus:get_users([alice, alice_bis, bob])) + end; init_per_group(user_session, Config) -> graphql_helper:init_user(Config). end_per_group(admin_session, Config) -> escalus:delete_users(Config, escalus:get_users([alice, alice_bis, bob])); +end_per_group(domain_admin_session, Config) -> + escalus:delete_users(Config, escalus:get_users([alice, alice_bis, bob])), + escalus_fresh:clean(), + graphql_helper:clean(); end_per_group(_GroupName, _Config) -> escalus_fresh:clean(), graphql_helper:clean(). @@ -97,6 +124,100 @@ user_sessions_info_story(Config, Alice) -> Path = [data, session, listSessions], ?assertMatch([#{<<"user">> := ExpectedUser}], get_ok_value(Path, Result)). + +domain_admin_list_sessions(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {bob, 1}], + fun domain_admin_list_sessions_story/4). + +domain_admin_list_sessions_story(Config, Alice, AliceB, _Bob) -> + Domain = escalus_client:server(Alice), + BisDomain = escalus_client:server(AliceB), + Path = [data, session, listSessions], + % List all sessions + Res = list_sessions(null, Config), + get_unauthorized(Res), + % List sessions for an external domain + Res2 = list_sessions(BisDomain, Config), + get_unauthorized(Res2), + % List sessions for local domain + Res3 = list_sessions(Domain, Config), + Sessions = get_ok_value(Path, Res3), + ?assertEqual(2, length(Sessions)). + +domain_admin_count_sessions(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {bob, 1}], + fun domain_admin_count_sessions_story/4). + +domain_admin_count_sessions_story(Config, Alice, AliceB, _Bob) -> + Domain = escalus_client:server(Alice), + BisDomain = escalus_client:server(AliceB), + Path = [data, session, countSessions], + % Count all sessions + Res = count_sessions(null, Config), + get_unauthorized(Res), + % Count sessions for an external domain + Res2 = count_sessions(BisDomain, Config), + get_unauthorized(Res2), + % Count sessions for local domain + Res3 = count_sessions(Domain, Config), + Number = get_ok_value(Path, Res3), + ?assertEqual(2, Number). + +domain_admin_list_users_with_status(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}], + fun domain_admin_list_users_with_status_story/3). + +domain_admin_list_users_with_status_story(Config, Alice, _AliceB) -> + AliceJID = escalus_client:full_jid(Alice), + Path = [data, session, listUsersWithStatus], + AwayStatus = <<"away">>, + AwayPresence = escalus_stanza:presence_show(AwayStatus), + DndStatus = <<"dnd">>, + DndPresence = escalus_stanza:presence_show(DndStatus), + % List users with away status globally + escalus_client:send(Alice, AwayPresence), + Res = list_users_with_status(null, AwayStatus, Config), + get_unauthorized(Res), + % List users with away status for a domain + Res2 = list_users_with_status(domain_helper:domain(), AwayStatus, Config), + StatusUsers = get_ok_value(Path, Res2), + ?assertEqual(1, length(StatusUsers)), + check_users([AliceJID], StatusUsers), + % List users with dnd status globally + escalus_client:send(Alice, DndPresence), + Res3 = list_users_with_status(null, DndStatus, Config), + get_unauthorized(Res3), + % List users with dnd status for a domain + Res4 = list_users_with_status(domain_helper:domain(), DndStatus, Config), + StatusUsers2 = get_ok_value(Path, Res4), + ?assertEqual(1, length(StatusUsers2)), + check_users([AliceJID], StatusUsers2). + +domain_admin_count_users_with_status(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}], + fun domain_admin_count_users_with_status_story/3). + +domain_admin_count_users_with_status_story(Config, Alice, _AliceB) -> + Path = [data, session, countUsersWithStatus], + AwayStatus = <<"away">>, + AwayPresence = escalus_stanza:presence_show(AwayStatus), + DndStatus = <<"dnd">>, + DndPresence = escalus_stanza:presence_show(DndStatus), + % Count users with away status globally + escalus_client:send(Alice, AwayPresence), + Res = count_users_with_status(null, AwayStatus, Config), + get_unauthorized(Res), + % Count users with away status for a domain + Res2 = count_users_with_status(domain_helper:domain(), AwayStatus, Config), + ?assertEqual(1, get_ok_value(Path, Res2)), + % Count users with dnd status globally + escalus_client:send(Alice, DndPresence), + Res3 = count_users_with_status(null, DndStatus, Config), + get_unauthorized(Res3), + % Count users with dnd status for a domain + Res4 = count_users_with_status(domain_helper:domain(), DndStatus, Config), + ?assertEqual(1, get_ok_value(Path, Res4)). + admin_list_sessions(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {bob, 1}], fun admin_list_sessions_story/4). diff --git a/big_tests/tests/graphql_stanza_SUITE.erl b/big_tests/tests/graphql_stanza_SUITE.erl index d11df13a157..6d0912c941e 100644 --- a/big_tests/tests/graphql_stanza_SUITE.erl +++ b/big_tests/tests/graphql_stanza_SUITE.erl @@ -7,7 +7,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(graphql_helper, [execute_user_command/5, execute_command/4, - get_ok_value/2, get_err_code/1, get_err_msg/1, get_coercion_err_msg/1]). + get_ok_value/2, get_err_code/1, get_err_msg/1, get_coercion_err_msg/1, + get_unauthorized/1]). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -15,11 +16,13 @@ suite() -> all() -> [{group, admin_stanza_http}, {group, admin_stanza_cli}, + {group, domain_admin_stanza}, {group, user_stanza}]. groups() -> [{admin_stanza_http, [parallel], admin_stanza_cases()}, {admin_stanza_cli, [], admin_stanza_cases()}, + {domain_admin_stanza, [], domain_admin_stanza_cases()}, {user_stanza, [parallel], user_stanza_cases()}]. admin_stanza_cases() -> @@ -40,6 +43,25 @@ admin_get_last_messages_cases() -> admin_get_last_messages_limit_enforced, admin_get_last_messages_before]. +domain_admin_stanza_cases() -> + [admin_send_message, + admin_send_message_to_unparsable_jid, + admin_send_message_headline, + domain_admin_send_stanza, + admin_send_unparsable_stanza, + domain_admin_send_stanza_from_unknown_user, + domain_admin_send_stanza_from_unknown_domain] + ++ domain_admin_get_last_messages_cases(). + +domain_admin_get_last_messages_cases() -> + [admin_get_last_messages, + admin_get_last_messages_for_unknown_user, + admin_get_last_messages_with, + admin_get_last_messages_limit, + admin_get_last_messages_limit_enforced, + admin_get_last_messages_before, + domain_admin_get_last_messages_no_permission]. + user_stanza_cases() -> [user_send_message, user_send_message_without_from, @@ -68,6 +90,8 @@ init_per_group(admin_stanza_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_stanza_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_stanza, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_stanza, Config) -> graphql_helper:init_user(Config). @@ -78,7 +102,8 @@ init_per_testcase(CaseName, Config) -> HasMam = proplists:get_value(has_mam, Config, false), MamOnly = lists:member(CaseName, user_get_last_messages_cases() - ++ admin_get_last_messages_cases()), + ++ admin_get_last_messages_cases() + ++ domain_admin_get_last_messages_cases()), case {HasMam, MamOnly} of {false, true} -> {skip, no_mam}; @@ -194,6 +219,47 @@ user_send_message_headline_with_spoofed_from_story(Config, Alice, Bob) -> Res = user_send_message_headline(Alice, From, To, <<"Welcome">>, <<"Hi!">>, Config), spoofed_error(sendMessageHeadLine, Res). +domain_admin_send_stanza(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_send_stanza_story/3). + +domain_admin_send_stanza_story(Config, Alice, Bob) -> + Body = <<"Hi!">>, + Stanza = escalus_stanza:from(escalus_stanza:chat_to_short_jid(Bob, Body), Alice), + Res = send_stanza(exml:to_binary(Stanza), Config), + get_unauthorized(Res). + +domain_admin_send_stanza_from_unknown_user(Config) -> + escalus:fresh_story_with_config(Config, [{bob, 1}], + fun domain_admin_send_stanza_from_unknown_user_story/2). + +domain_admin_send_stanza_from_unknown_user_story(Config, Bob) -> + Body = <<"Hi!">>, + Server = escalus_client:server(Bob), + From = <<"YeeeAH@", Server/binary>>, + Stanza = escalus_stanza:from(escalus_stanza:chat_to_short_jid(Bob, Body), From), + Res = send_stanza(exml:to_binary(Stanza), Config), + get_unauthorized(Res). + +domain_admin_send_stanza_from_unknown_domain(Config) -> + escalus:fresh_story_with_config(Config, [{bob, 1}], + fun domain_admin_send_stanza_from_unknown_domain_story/2). + +domain_admin_send_stanza_from_unknown_domain_story(Config, Bob) -> + Body = <<"Hi!">>, + From = <<"YeeeAH@oopsie">>, + Stanza = escalus_stanza:from(escalus_stanza:chat_to_short_jid(Bob, Body), From), + Res = send_stanza(exml:to_binary(Stanza), Config), + get_unauthorized(Res). + +domain_admin_get_last_messages_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_get_last_messages_story_no_permission/2). + +domain_admin_get_last_messages_story_no_permission(Config, AliceBis) -> + Res = get_last_messages(escalus_client:full_jid(AliceBis), null, null, Config), + get_unauthorized(Res). + admin_send_stanza(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_send_stanza_story/3). diff --git a/big_tests/tests/graphql_token_SUITE.erl b/big_tests/tests/graphql_token_SUITE.erl index fc3641d185d..9a95e624545 100644 --- a/big_tests/tests/graphql_token_SUITE.erl +++ b/big_tests/tests/graphql_token_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [require_rpc_nodes/1, mim/0]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, - get_ok_value/2, get_err_code/1]). + get_ok_value/2, get_err_code/1, get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). @@ -13,11 +13,13 @@ suite() -> all() -> [{group, user}, + {group, domain_admin}, {group, admin_http}, {group, admin_cli}]. groups() -> [{user, [], user_tests()}, + {domain_admin, domain_admin_tests()}, {admin_http, [], admin_tests()}, {admin_cli, [], admin_tests()}]. @@ -26,6 +28,13 @@ user_tests() -> user_revoke_token_no_token_before_test, user_revoke_token_test]. +domain_admin_tests() -> + [admin_request_token_test, + domain_admin_request_token_no_user_test, + domain_admin_revoke_token_no_user_test, + admin_revoke_token_no_token_test, + admin_revoke_token_test]. + admin_tests() -> [admin_request_token_test, admin_request_token_no_user_test, @@ -65,6 +74,8 @@ init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user, Config) -> graphql_helper:init_user(Config). @@ -106,6 +117,14 @@ user_revoke_token_test(Config, Alice) -> ParsedRes = get_ok_value([data, token, revokeToken], Res2), ?assertEqual(<<"Revoked.">>, ParsedRes). +% Domain admin tests + +domain_admin_request_token_no_user_test(Config) -> + get_unauthorized(admin_request_token(<<"AAAAA">>, Config)). + +domain_admin_revoke_token_no_user_test(Config) -> + get_unauthorized(admin_revoke_token(<<"AAAAA">>, Config)). + % Admin tests admin_request_token_test(Config) -> diff --git a/big_tests/tests/graphql_vcard_SUITE.erl b/big_tests/tests/graphql_vcard_SUITE.erl index ed506a500fb..eb7ec5c9aee 100644 --- a/big_tests/tests/graphql_vcard_SUITE.erl +++ b/big_tests/tests/graphql_vcard_SUITE.erl @@ -4,7 +4,8 @@ -import(distributed_helper, [require_rpc_nodes/1, mim/0]). -import(graphql_helper, [execute_command/4, execute_user_command/5, - user_to_bin/1, get_ok_value/2, skip_null_fields/1, get_err_msg/1]). + user_to_bin/1, get_ok_value/2, skip_null_fields/1, get_err_msg/1, + get_unauthorized/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -14,11 +15,13 @@ suite() -> all() -> [{group, user_vcard}, + {group, domain_admin_vcard}, {group, admin_vcard_http}, {group, admin_vcard_cli}]. groups() -> [{user_vcard, [], user_vcard_tests()}, + {domain_admin_vcard, [], domain_admin_vcard_tests()}, {admin_vcard_http, [], admin_vcard_tests()}, {admin_vcard_cli, [], admin_vcard_tests()}]. @@ -30,6 +33,16 @@ user_vcard_tests() -> user_get_others_vcard_no_user, user_get_others_vcard_no_vcard]. +domain_admin_vcard_tests() -> + [admin_set_vcard, + admin_set_vcard_incomplete_fields, + domain_admin_set_vcard_no_permission, + domain_admin_set_vcard_no_user, + admin_get_vcard, + admin_get_vcard_no_vcard, + domain_admin_get_vcard_no_user, + domain_admin_get_vcard_no_permission]. + admin_vcard_tests() -> [admin_set_vcard, admin_set_vcard_incomplete_fields, @@ -56,6 +69,8 @@ init_per_group(admin_vcard_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_vcard_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(domain_admin_vcard, Config) -> + graphql_helper:init_domain_admin_handler(Config); init_per_group(user_vcard, Config) -> graphql_helper:init_user(Config). @@ -141,6 +156,32 @@ user_get_others_vcard_no_user(Config, Alice) -> Result = user_get_vcard(Alice, <<"AAAAA">>, Config), ?assertEqual(<<"User does not exist">>, get_err_msg(Result)). +%% Domain admin test cases + +domain_admin_set_vcard_no_user(Config) -> + Vcard = complete_vcard_input(), + get_unauthorized(set_vcard(Vcard, <<"AAAAA">>, Config)). + +domain_admin_get_vcard_no_user(Config) -> + get_unauthorized(get_vcard(<<"AAAAA">>, Config)). + +domain_admin_get_vcard_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_get_vcard_no_permission/2). + +domain_admin_get_vcard_no_permission(Config, AliceBis) -> + Result = get_vcard(user_to_bin(AliceBis), Config), + get_unauthorized(Result). + +domain_admin_set_vcard_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_set_vcard_no_permission/2). + +domain_admin_set_vcard_no_permission(Config, Alice) -> + Vcard = complete_vcard_input(), + Result = set_vcard(Vcard, user_to_bin(Alice), Config), + get_unauthorized(Result). + %% Admin test cases admin_set_vcard(Config) -> From 81305973ad1c1aa2c1bfe260dc7be933d65653cd Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 9 Sep 2022 13:26:47 +0200 Subject: [PATCH 005/117] Implement broadcasts in async pools There is two new interfaces: one simply broadcasts a new value given a key to all workers. The other, broadcasts new important data that should be aggregated to all available keys. This is useful if we need to modify whatever tasks are pending. --- .../mongoose_aggregator_worker.erl | 20 ++++++++++ src/async_pools/mongoose_async_pools.erl | 14 ++++++- src/async_pools/mongoose_batch_worker.erl | 2 + test/batches_SUITE.erl | 39 +++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/async_pools/mongoose_aggregator_worker.erl b/src/async_pools/mongoose_aggregator_worker.erl index fcabb679a17..30253471c93 100644 --- a/src/async_pools/mongoose_aggregator_worker.erl +++ b/src/async_pools/mongoose_aggregator_worker.erl @@ -82,6 +82,8 @@ handle_call(Msg, From, State) -> -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({task, Key, Value}, State) -> {noreply, handle_task(Key, Value, State)}; +handle_cast({broadcast, Broadcast}, State) -> + {noreply, handle_broadcast(Broadcast, State)}; handle_cast(Msg, State) -> ?UNEXPECTED_CAST(Msg), {noreply, State}. @@ -148,6 +150,24 @@ handle_task(Key, NewValue, #state{aggregate_callback = Aggregator, flush_queue = queue:in(Key, Queue)} end. +% If we don't have any request pending, it means that it is the first task submitted, +% so aggregation is not needed. +handle_broadcast(Task, #state{async_request = no_request_pending} = State) -> + State#state{async_request = make_async_request(Task, State)}; +handle_broadcast(Task, #state{aggregate_callback = Aggregator, + flush_elems = Acc, + flush_extra = Extra} = State) -> + Map = fun(_Key, OldValue) -> + case Aggregator(OldValue, Task, Extra) of + {ok, FinalValue} -> + FinalValue; + {error, Reason} -> + ?LOG_ERROR(log_fields(State, #{what => aggregation_failed, reason => Reason})), + OldValue + end + end, + State#state{flush_elems = maps:map(Map, Acc)}. + maybe_request_next(#state{flush_elems = Acc, flush_queue = Queue} = State) -> case queue:out(Queue) of {{value, Key}, NewQueue} -> diff --git a/src/async_pools/mongoose_async_pools.erl b/src/async_pools/mongoose_async_pools.erl index adfd1ad73d1..5aa7b124cc4 100644 --- a/src/async_pools/mongoose_async_pools.erl +++ b/src/async_pools/mongoose_async_pools.erl @@ -8,8 +8,8 @@ % API -export([start_pool/3, stop_pool/2]). --export([put_task/3, put_task/4]). --ignore_xref([put_task/3]). +-export([put_task/3, put_task/4, broadcast/3, broadcast_task/4]). +-ignore_xref([put_task/3, broadcast/3, broadcast_task/4]). -export([sync/2]). -type task() :: term(). @@ -55,6 +55,16 @@ put_task(HostType, PoolId, Key, Task) -> PoolName = pool_name(HostType, PoolId), wpool:cast(PoolName, {task, Key, Task}, {hash_worker, Key}). +-spec broadcast(mongooseim:host_type(), pool_id(), term()) -> ok. +broadcast(HostType, PoolId, Task) -> + PoolName = pool_name(HostType, PoolId), + wpool:broadcast(PoolName, {broadcast, Task}). + +-spec broadcast_task(mongooseim:host_type(), pool_id(), term(), term()) -> ok. +broadcast_task(HostType, PoolId, Key, Task) -> + PoolName = pool_name(HostType, PoolId), + wpool:broadcast(PoolName, {task, Key, Task}). + %%% API functions -spec start_pool(mongooseim:host_type(), pool_id(), pool_opts()) -> supervisor:startchild_ret(). diff --git a/src/async_pools/mongoose_batch_worker.erl b/src/async_pools/mongoose_batch_worker.erl index 047647ad35c..d55379e5ab7 100644 --- a/src/async_pools/mongoose_batch_worker.erl +++ b/src/async_pools/mongoose_batch_worker.erl @@ -74,6 +74,8 @@ handle_cast({task, Task}, State) -> {noreply, handle_task(Task, State)}; handle_cast({task, _Key, Task}, State) -> {noreply, handle_task(Task, State)}; +handle_cast({broadcast, Broadcast}, State) -> + {noreply, handle_task(Broadcast, State)}; handle_cast(Msg, State) -> ?UNEXPECTED_CAST(Msg), {noreply, State}. diff --git a/test/batches_SUITE.erl b/test/batches_SUITE.erl index 14821a74e14..f0b76ef8b1e 100644 --- a/test/batches_SUITE.erl +++ b/test/batches_SUITE.erl @@ -23,6 +23,8 @@ groups() -> ]}, {async_workers, [sequence], [ + broadcast_reaches_all_workers, + broadcast_reaches_all_keys, filled_batch_raises_batch_metric, unfilled_batch_raises_flush_metric, timeouts_and_canceled_timers_do_not_need_to_log_messages, @@ -107,6 +109,43 @@ shared_cache_inserts_in_shared_table(_) -> mongoose_user_cache:merge_entry(host_type(), ?mod(2), some_jid(), #{}), ?assert(mongoose_user_cache:is_member(host_type(), ?mod(1), some_jid())). +broadcast_reaches_all_workers(_) -> + {ok, Server} = gen_server:start_link(?MODULE, [], []), + WPoolOpts = #{pool_type => aggregate, + pool_size => 10, + request_callback => fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, + aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, + verify_callback => fun(ok, _T, _) -> ok end}, + {ok, _} = mongoose_async_pools:start_pool(host_type(), ?FUNCTION_NAME, WPoolOpts), + mongoose_async_pools:broadcast_task(host_type(), ?FUNCTION_NAME, key, 1), + async_helper:wait_until( + fun() -> gen_server:call(Server, get_acc) end, 10). + +broadcast_reaches_all_keys(_) -> + HostType = host_type(), + {ok, Server} = gen_server:start_link(?MODULE, [], []), + Tid = ets:new(table, [public, {read_concurrency, true}]), + Req = fun(Task, _) -> + case ets:member(Tid, continue) of + true -> + gen_server:send_request(Server, Task); + false -> + async_helper:wait_until(fun() -> ets:member(Tid, continue) end, true), + gen_server:send_request(Server, 0) + end + end, + WPoolOpts = #{pool_type => aggregate, + pool_size => 3, + request_callback => Req, + aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, + verify_callback => fun(ok, _T, _) -> ok end}, + {ok, _} = mongoose_async_pools:start_pool(HostType, ?FUNCTION_NAME, WPoolOpts), + [ mongoose_async_pools:put_task(HostType, ?FUNCTION_NAME, N, 1) || N <- lists:seq(0, 1000) ], + mongoose_async_pools:broadcast(HostType, ?FUNCTION_NAME, -1), + ets:insert(Tid, {continue, true}), + async_helper:wait_until( + fun() -> gen_server:call(Server, get_acc) end, 0). + filled_batch_raises_batch_metric(_) -> Opts = #{host_type => host_type(), pool_id => ?FUNCTION_NAME, From 9c2c13875a80dbd0d2eec13a5726a4289c3da46f Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 17:12:44 +0200 Subject: [PATCH 006/117] OS extend aggregator requests --- .../mongoose_aggregator_worker.erl | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/async_pools/mongoose_aggregator_worker.erl b/src/async_pools/mongoose_aggregator_worker.erl index 30253471c93..414080c2f6c 100644 --- a/src/async_pools/mongoose_aggregator_worker.erl +++ b/src/async_pools/mongoose_aggregator_worker.erl @@ -21,7 +21,7 @@ mongoose_async_pools:pool_extra()) -> {ok, mongoose_async_pools:task()} | {error, term()}. -callback request(mongoose_async_pools:task(), mongoose_async_pools:pool_extra()) -> - gen_server:request_id(). + gen_server:request_id() | drop. -callback verify(term(), mongoose_async_pools:task(), mongoose_async_pools:pool_extra()) -> term(). -optional_callbacks([verify/3]). @@ -55,16 +55,16 @@ -spec init(map()) -> {ok, state()}. init(#{host_type := HostType, pool_id := PoolId, - request_callback := Requester, + request_callback := Requestor, aggregate_callback := Aggregator, flush_extra := FlushExtra} = Opts) - when is_function(Requester, 2), + when is_function(Requestor, 2), is_function(Aggregator, 3), is_map(FlushExtra) -> ?LOG_DEBUG(#{what => aggregator_worker_start, host_type => HostType, pool_id => PoolId}), {ok, #state{host_type = HostType, pool_id = PoolId, - request_callback = Requester, + request_callback = Requestor, aggregate_callback = Aggregator, verify_callback = maps:get(verify_callback, Opts, undefined), flush_extra = FlushExtra}}. @@ -129,7 +129,7 @@ format_status(_Opt, [_PDict, State | _]) -> % If we don't have any request pending, it means that it is the first task submitted, % so aggregation is not needed. handle_task(_, Value, #state{async_request = no_request_pending} = State) -> - State#state{async_request = make_async_request(Value, State)}; + make_async_request(Value, State); handle_task(Key, NewValue, #state{aggregate_callback = Aggregator, flush_elems = Acc, flush_queue = Queue, @@ -153,7 +153,7 @@ handle_task(Key, NewValue, #state{aggregate_callback = Aggregator, % If we don't have any request pending, it means that it is the first task submitted, % so aggregation is not needed. handle_broadcast(Task, #state{async_request = no_request_pending} = State) -> - State#state{async_request = make_async_request(Task, State)}; + make_async_request(Task, State); handle_broadcast(Task, #state{aggregate_callback = Aggregator, flush_elems = Acc, flush_extra = Extra} = State) -> @@ -172,16 +172,21 @@ maybe_request_next(#state{flush_elems = Acc, flush_queue = Queue} = State) -> case queue:out(Queue) of {{value, Key}, NewQueue} -> {Value, NewAcc} = maps:take(Key, Acc), - State#state{async_request = make_async_request(Value, State), - flush_elems = NewAcc, flush_queue = NewQueue}; + NewState = make_async_request(Value, State), + NewState#state{flush_elems = NewAcc, flush_queue = NewQueue}; {empty, _} -> State#state{async_request = no_request_pending} end. -make_async_request(Value, #state{host_type = HostType, pool_id = PoolId, - request_callback = Requestor, flush_extra = Extra}) -> - mongoose_metrics:update(HostType, [mongoose_async_pools, PoolId, async_request], 1), - {Requestor(Value, Extra), Value}. +make_async_request(Request, #state{host_type = HostType, pool_id = PoolId, + request_callback = Requestor, flush_extra = Extra} = State) -> + case Requestor(Request, Extra) of + drop -> + State; + ReqId -> + mongoose_metrics:update(HostType, [mongoose_async_pools, PoolId, async_request], 1), + State#state{async_request = {ReqId, Request}} + end. maybe_verify_reply(_, _, #state{verify_callback = undefined}) -> ok; From 552a71345b7838d60908d4a34bb0f7ff21d37d2a Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 22:22:02 +0200 Subject: [PATCH 007/117] OS fix return type for transaction request --- src/rdbms/mongoose_rdbms.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rdbms/mongoose_rdbms.erl b/src/rdbms/mongoose_rdbms.erl index 1003f49242f..1d0c810683e 100644 --- a/src/rdbms/mongoose_rdbms.erl +++ b/src/rdbms/mongoose_rdbms.erl @@ -288,7 +288,7 @@ sql_transaction(HostType, F) when is_function(F) -> sql_call(HostType, {sql_transaction, F}). %% @doc SQL transaction based on a list of queries --spec sql_transaction_request(server(), fun() | maybe_improper_list()) -> transaction_result(). +-spec sql_transaction_request(server(), fun() | maybe_improper_list()) -> gen_server:request_id(). sql_transaction_request(HostType, Queries) when is_list(Queries) -> F = fun() -> lists:map(fun sql_query_t/1, Queries) end, sql_transaction_request(HostType, F); From 286499618dd2227f97583728394a0d8312847af3 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 19:14:18 +0200 Subject: [PATCH 008/117] Test the aggregation --- src/async_pools/mongoose_aggregator_worker.erl | 2 +- test/batches_SUITE.erl | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/async_pools/mongoose_aggregator_worker.erl b/src/async_pools/mongoose_aggregator_worker.erl index 414080c2f6c..bc03b700241 100644 --- a/src/async_pools/mongoose_aggregator_worker.erl +++ b/src/async_pools/mongoose_aggregator_worker.erl @@ -179,7 +179,7 @@ maybe_request_next(#state{flush_elems = Acc, flush_queue = Queue} = State) -> end. make_async_request(Request, #state{host_type = HostType, pool_id = PoolId, - request_callback = Requestor, flush_extra = Extra} = State) -> + request_callback = Requestor, flush_extra = Extra} = State) -> case Requestor(Request, Extra) of drop -> State; diff --git a/test/batches_SUITE.erl b/test/batches_SUITE.erl index f0b76ef8b1e..e03c4eab6b9 100644 --- a/test/batches_SUITE.erl +++ b/test/batches_SUITE.erl @@ -32,6 +32,7 @@ groups() -> sync_flushes_down_everything, sync_aggregates_down_everything, aggregating_error_is_handled, + aggregation_might_produce_noop_requests, async_request ]} ]. @@ -109,6 +110,21 @@ shared_cache_inserts_in_shared_table(_) -> mongoose_user_cache:merge_entry(host_type(), ?mod(2), some_jid(), #{}), ?assert(mongoose_user_cache:is_member(host_type(), ?mod(1), some_jid())). +aggregation_might_produce_noop_requests(_) -> + {ok, Server} = gen_server:start_link(?MODULE, [], []), + Requestor = fun(0, _) -> timer:sleep(1), gen_server:send_request(Server, 0); + (_, _) -> drop end, + Aggregator = fun(T1, T2, _) -> {ok, T1 + T2} end, + WPoolOpts = #{pool_type => aggregate, + pool_size => 10, + request_callback => Requestor, + aggregate_callback => Aggregator, + verify_callback => fun(ok, _T, _) -> ok end}, + {ok, _} = mongoose_async_pools:start_pool(host_type(), ?FUNCTION_NAME, WPoolOpts), + mongoose_async_pools:broadcast_task(host_type(), ?FUNCTION_NAME, key, 1), + async_helper:wait_until( + fun() -> gen_server:call(Server, get_acc) end, 0). + broadcast_reaches_all_workers(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), WPoolOpts = #{pool_type => aggregate, From 353186e99cb1b41095d0c0496a96924177a07007 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 23:33:06 +0200 Subject: [PATCH 009/117] Cleanup async tests --- test/batches_SUITE.erl | 74 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/test/batches_SUITE.erl b/test/batches_SUITE.erl index e03c4eab6b9..34864dd9db7 100644 --- a/test/batches_SUITE.erl +++ b/test/batches_SUITE.erl @@ -112,26 +112,19 @@ shared_cache_inserts_in_shared_table(_) -> aggregation_might_produce_noop_requests(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), - Requestor = fun(0, _) -> timer:sleep(1), gen_server:send_request(Server, 0); + Requestor = fun(1, _) -> timer:sleep(1), gen_server:send_request(Server, 1); (_, _) -> drop end, - Aggregator = fun(T1, T2, _) -> {ok, T1 + T2} end, - WPoolOpts = #{pool_type => aggregate, - pool_size => 10, - request_callback => Requestor, - aggregate_callback => Aggregator, - verify_callback => fun(ok, _T, _) -> ok end}, - {ok, _} = mongoose_async_pools:start_pool(host_type(), ?FUNCTION_NAME, WPoolOpts), - mongoose_async_pools:broadcast_task(host_type(), ?FUNCTION_NAME, key, 1), + Opts = (default_aggregator_opts(Server))#{pool_id => ?FUNCTION_NAME, + request_callback => Requestor}, + {ok, Pid} = gen_server:start_link(mongoose_aggregator_worker, Opts, []), + [ gen_server:cast(Pid, {task, key, N}) || N <- lists:seq(1, 1000) ], async_helper:wait_until( - fun() -> gen_server:call(Server, get_acc) end, 0). + fun() -> gen_server:call(Server, get_acc) end, 1). broadcast_reaches_all_workers(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), - WPoolOpts = #{pool_type => aggregate, - pool_size => 10, - request_callback => fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, - aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, - verify_callback => fun(ok, _T, _) -> ok end}, + WPoolOpts = (default_aggregator_opts(Server))#{pool_type => aggregate, + pool_size => 10}, {ok, _} = mongoose_async_pools:start_pool(host_type(), ?FUNCTION_NAME, WPoolOpts), mongoose_async_pools:broadcast_task(host_type(), ?FUNCTION_NAME, key, 1), async_helper:wait_until( @@ -150,11 +143,9 @@ broadcast_reaches_all_keys(_) -> gen_server:send_request(Server, 0) end end, - WPoolOpts = #{pool_type => aggregate, - pool_size => 3, - request_callback => Req, - aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, - verify_callback => fun(ok, _T, _) -> ok end}, + WPoolOpts = (default_aggregator_opts(Server))#{pool_type => aggregate, + pool_size => 3, + request_callback => Req}, {ok, _} = mongoose_async_pools:start_pool(HostType, ?FUNCTION_NAME, WPoolOpts), [ mongoose_async_pools:put_task(HostType, ?FUNCTION_NAME, N, 1) || N <- lists:seq(0, 1000) ], mongoose_async_pools:broadcast(HostType, ?FUNCTION_NAME, -1), @@ -243,12 +234,7 @@ sync_flushes_down_everything(_) -> sync_aggregates_down_everything(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), - Opts = #{host_type => host_type(), - pool_id => ?FUNCTION_NAME, - request_callback => fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, - aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, - verify_callback => fun(ok, _T, _) -> ok end, - flush_extra => #{host_type => host_type()}}, + Opts = (default_aggregator_opts(Server))#{pool_id => ?FUNCTION_NAME}, {ok, Pid} = gen_server:start_link(mongoose_aggregator_worker, Opts, []), ?assertEqual(skipped, gen_server:call(Pid, sync)), [ gen_server:cast(Pid, {task, key, N}) || N <- lists:seq(1, 1000) ], @@ -257,12 +243,9 @@ sync_aggregates_down_everything(_) -> aggregating_error_is_handled(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), - Opts = #{host_type => host_type(), - pool_id => ?FUNCTION_NAME, - request_callback => fun(_, _) -> gen_server:send_request(Server, return_error) end, - aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, - verify_callback => fun(ok, _T, _) -> ok end, - flush_extra => #{host_type => host_type()}}, + Requestor = fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, + Opts = (default_aggregator_opts(Server))#{pool_id => ?FUNCTION_NAME, + request_callback => Requestor}, {ok, Pid} = gen_server:start_link(mongoose_aggregator_worker, Opts, []), gen_server:cast(Pid, {task, key, 0}), async_helper:wait_until( @@ -270,12 +253,7 @@ aggregating_error_is_handled(_) -> async_request(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), - Opts = #{host_type => host_type(), - pool_id => ?FUNCTION_NAME, - request_callback => fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, - aggregate_callback => fun(T1, T2, _) -> {ok, T1 + T2} end, - verify_callback => fun(ok, _T, _) -> ok end, - flush_extra => #{host_type => host_type()}}, + Opts = (default_aggregator_opts(Server))#{pool_id => ?FUNCTION_NAME}, {ok, Pid} = gen_server:start_link(mongoose_aggregator_worker, Opts, []), [ gen_server:cast(Pid, {task, key, N}) || N <- lists:seq(1, 1000) ], async_helper:wait_until( @@ -288,6 +266,26 @@ host_type() -> some_jid() -> jid:make_noprep(<<"alice">>, <<"localhost">>, <<>>). +default_aggregator_opts(Server) -> + #{host_type => host_type(), + request_callback => requester(Server), + aggregate_callback => fun aggregate_sum/3, + verify_callback => fun validate_all_ok/3, + flush_extra => #{host_type => host_type()}}. + +validate_all_ok(ok, _, _) -> + ok. + +aggregate_sum(T1, T2, _) -> + {ok, T1 + T2}. + +requester(Server) -> + fun(return_error, _) -> + gen_server:send_request(Server, return_error); + (Task, _) -> + timer:sleep(1), gen_server:send_request(Server, Task) + end. + init([]) -> {ok, 0}. From b43756b35a40f69ffaf78d46bdad07d57c35b284 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 23:33:33 +0200 Subject: [PATCH 010/117] Test for bug where an error stops the aggregator! --- test/batches_SUITE.erl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/batches_SUITE.erl b/test/batches_SUITE.erl index 34864dd9db7..2700e899774 100644 --- a/test/batches_SUITE.erl +++ b/test/batches_SUITE.erl @@ -31,7 +31,7 @@ groups() -> prepare_task_works, sync_flushes_down_everything, sync_aggregates_down_everything, - aggregating_error_is_handled, + aggregating_error_is_handled_and_can_continue, aggregation_might_produce_noop_requests, async_request ]} @@ -241,15 +241,20 @@ sync_aggregates_down_everything(_) -> ?assertEqual(ok, gen_server:call(Pid, sync)), ?assertEqual(500500, gen_server:call(Server, get_acc)). -aggregating_error_is_handled(_) -> +aggregating_error_is_handled_and_can_continue(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), Requestor = fun(Task, _) -> timer:sleep(1), gen_server:send_request(Server, Task) end, Opts = (default_aggregator_opts(Server))#{pool_id => ?FUNCTION_NAME, request_callback => Requestor}, {ok, Pid} = gen_server:start_link(mongoose_aggregator_worker, Opts, []), - gen_server:cast(Pid, {task, key, 0}), - async_helper:wait_until( - fun() -> gen_server:call(Server, get_acc) end, 0). + [ gen_server:cast(Pid, {task, key, N}) || N <- lists:seq(1, 10) ], + gen_server:cast(Pid, {task, return_error, return_error}), + ct:sleep(100), + [ gen_server:cast(Pid, {task, key, N}) || N <- lists:seq(11, 100) ], + %% We don't call sync here because sync is force flushing, + %% we want to test that it flushes alone + ct:sleep(100), + ?assert(55 < gen_server:call(Server, get_acc)). async_request(_) -> {ok, Server} = gen_server:start_link(?MODULE, [], []), From b198cb058a0376700bd17be1dd92c1fede8830b5 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 12 Sep 2022 23:35:43 +0200 Subject: [PATCH 011/117] Ensure to keep flushing until a request does not drop --- src/async_pools/mongoose_aggregator_worker.erl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/async_pools/mongoose_aggregator_worker.erl b/src/async_pools/mongoose_aggregator_worker.erl index bc03b700241..2d489062a7c 100644 --- a/src/async_pools/mongoose_aggregator_worker.erl +++ b/src/async_pools/mongoose_aggregator_worker.erl @@ -96,10 +96,10 @@ handle_info(Msg, #state{async_request = {AsyncRequest, ReqTask}} = State) -> case gen_server:check_response(Msg, AsyncRequest) of {error, {Reason, _Ref}} -> ?LOG_ERROR(log_fields(State, #{what => asynchronous_request_failed, reason => Reason})), - {noreply, State}; + {noreply, State#state{async_request = no_request_pending}}; {reply, {error, Reason}} -> ?LOG_ERROR(log_fields(State, #{what => asynchronous_request_failed, reason => Reason})), - {noreply, State}; + {noreply, State#state{async_request = no_request_pending}}; {reply, Reply} -> maybe_verify_reply(Reply, ReqTask, State), {noreply, maybe_request_next(State)}; @@ -172,8 +172,13 @@ maybe_request_next(#state{flush_elems = Acc, flush_queue = Queue} = State) -> case queue:out(Queue) of {{value, Key}, NewQueue} -> {Value, NewAcc} = maps:take(Key, Acc), - NewState = make_async_request(Value, State), - NewState#state{flush_elems = NewAcc, flush_queue = NewQueue}; + NewState1 = State#state{flush_elems = NewAcc, flush_queue = NewQueue}, + case make_async_request(Value, NewState1) of + NewState2 = #state{async_request = no_request_pending} -> + maybe_request_next(NewState2); + NewState2 -> + NewState2 + end; {empty, _} -> State#state{async_request = no_request_pending} end. From 3d55ec5fa40380d051297707f5174fe4ac08da5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Tue, 13 Sep 2022 11:43:12 +0200 Subject: [PATCH 012/117] Refactored disco_local_features hook to a gen_hook format --- src/ejabberd_local.erl | 22 ++++++++++++---------- src/inbox/mod_inbox.erl | 30 ++++++++++++++++++------------ src/mam/mod_mam_pm.erl | 32 +++++++++++++++++++------------- src/mod_adhoc.erl | 36 +++++++++++++++++++++--------------- src/mod_amp.erl | 27 +++++++++++++++++---------- src/mod_auth_token.erl | 28 +++++++++++++++++----------- src/mod_blocking.erl | 30 ++++++++++++++++++------------ src/mod_caps.erl | 27 +++++++++++++++++---------- src/mod_carboncopy.erl | 31 +++++++++++++++++++------------ src/mod_disco.erl | 28 +++++++++++++++++----------- src/mongoose_hooks.erl | 2 +- src/privacy/mod_privacy.erl | 28 +++++++++++++++++----------- src/pubsub/mod_pubsub.erl | 29 ++++++++++++++++++----------- 13 files changed, 211 insertions(+), 139 deletions(-) diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 0195f92ee16..7817b45f677 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -54,7 +54,7 @@ %% Hooks callbacks --export([disco_local_features/1]). +-export([disco_local_features/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -62,7 +62,7 @@ -export([do_route/4]). --ignore_xref([disco_local_features/1, do_route/4, get_iq_callback/1, +-ignore_xref([disco_local_features/3, do_route/4, get_iq_callback/1, process_iq_reply/4, start_link/0]). -include("mongoose.hrl"). @@ -241,12 +241,14 @@ register_host(Host) -> unregister_host(Host) -> gen_server:call(?MODULE, {unregister_host, Host}). --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}) -> +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}, _, _) -> Features = [Feature || {_, Feature} <- ets:lookup(?NSTABLE, LServer)], - mongoose_disco:add_features(Features, Acc); -disco_local_features(Acc) -> - Acc. + {ok, mongoose_disco:add_features(Features, Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. %%==================================================================== %% gen_server callbacks @@ -263,7 +265,7 @@ init([]) -> catch ets:new(?IQTABLE, [named_table, protected, {read_concurrency, true}]), catch ets:new(?NSTABLE, [named_table, bag, protected, {read_concurrency, true}]), catch ets:new(?IQRESPONSE, [named_table, public]), - ejabberd_hooks:add(hooks()), + gen_hook:add_handlers(hooks()), {ok, #state{}}. %%-------------------------------------------------------------------- @@ -337,7 +339,7 @@ handle_info(_Info, State) -> %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> - ejabberd_hooks:delete(hooks()). + gen_hook:delete_handlers(hooks()). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -351,7 +353,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- hooks() -> - [{disco_local_features, HostType, ?MODULE, disco_local_features, 99} + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99} || HostType <- ?ALL_HOST_TYPES]. -spec do_route(Acc :: mongoose_acc:t(), diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index bff15a8e4d8..0d7e9d6f7cd 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -27,11 +27,11 @@ inbox_unread_count/2, remove_user/3, remove_domain/3, - disco_local_features/1 + disco_local_features/3 ]). -ignore_xref([ - disco_local_features/1, filter_local_packet/1, get_personal_data/3, + disco_local_features/3, filter_local_packet/1, get_personal_data/3, inbox_unread_count/2, remove_domain/3, remove_user/3, user_send_packet/4 ]). @@ -89,7 +89,8 @@ process_entry(#{remote_jid := RemJID, start(HostType, #{iqdisc := IQDisc, groupchat := MucTypes} = Opts) -> mod_inbox_backend:init(HostType, Opts), lists:member(muc, MucTypes) andalso mod_inbox_muc:start(HostType), - ejabberd_hooks:add(hooks(HostType)), + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm, fun ?MODULE:process_iq/5, #{}, IQDisc), gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm, @@ -101,7 +102,8 @@ start(HostType, #{iqdisc := IQDisc, groupchat := MucTypes} = Opts) -> stop(HostType) -> gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm), gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm), - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), stop_cleaner(HostType), mod_inbox_muc:stop(HostType), case mongoose_config:get_opt([{modules, HostType}, ?MODULE, backend]) of @@ -265,11 +267,13 @@ remove_domain(Acc, HostType, Domain) -> mod_inbox_backend:remove_domain(HostType, Domain), Acc. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_ESL_INBOX], Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([?NS_ESL_INBOX], Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc} . -spec maybe_process_message(Acc :: mongoose_acc:t(), From :: jid:jid(), @@ -546,17 +550,19 @@ get_inbox_unread(undefined, Acc, To) -> {ok, Count} = mod_inbox_backend:get_inbox_unread(HostType, InboxEntryKey), mongoose_acc:set(inbox, unread_count, Count, Acc). -hooks(HostType) -> +legacy_hooks(HostType) -> [ {remove_user, HostType, ?MODULE, remove_user, 50}, {remove_domain, HostType, ?MODULE, remove_domain, 50}, {user_send_packet, HostType, ?MODULE, user_send_packet, 70}, {filter_local_packet, HostType, ?MODULE, filter_local_packet, 90}, {inbox_unread_count, HostType, ?MODULE, inbox_unread_count, 80}, - {get_personal_data, HostType, ?MODULE, get_personal_data, 50}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 99} + {get_personal_data, HostType, ?MODULE, get_personal_data, 50} ]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + get_groupchat_types(HostType) -> gen_mod:get_module_opt(HostType, ?MODULE, groupchat). diff --git a/src/mam/mod_mam_pm.erl b/src/mam/mod_mam_pm.erl index 6588bb2e205..ebf5fc938e4 100644 --- a/src/mam/mod_mam_pm.erl +++ b/src/mam/mod_mam_pm.erl @@ -46,7 +46,7 @@ -export([start/2, stop/1, supported_features/0]). %% ejabberd handlers --export([disco_local_features/1, +-export([disco_local_features/3, process_mam_iq/5, user_send_packet/4, remove_user/3, @@ -64,7 +64,7 @@ -ignore_xref([archive_message_from_ct/1, archive_size/2, archive_size_with_host_type/3, delete_archive/2, - determine_amp_strategy/5, disco_local_features/1, filter_packet/1, + determine_amp_strategy/5, disco_local_features/3, filter_packet/1, get_personal_data/3, remove_user/3, sm_filter_offline_message/4, user_send_packet/4]). @@ -154,14 +154,16 @@ archive_id(Server, User) start(HostType, Opts) -> ?LOG_INFO(#{what => mam_starting, host_type => HostType}), ensure_metrics(HostType), - ejabberd_hooks:add(hooks(HostType)), + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), add_iq_handlers(HostType, Opts), ok. -spec stop(host_type()) -> any(). stop(HostType) -> ?LOG_INFO(#{what => mam_stopping, host_type => HostType}), - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), remove_iq_handlers(HostType), ok. @@ -202,11 +204,13 @@ process_mam_iq(Acc, From, To, IQ, _Extra) -> {Acc, return_action_not_allowed_error_iq(IQ)} end. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{host_type := HostType, node := <<>>}) -> - mongoose_disco:add_features(features(?MODULE, HostType), Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{host_type := HostType, node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features(features(?MODULE, HostType), Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. %% @doc Handle an outgoing message. %% @@ -679,10 +683,9 @@ is_archivable_message(HostType, Dir, Packet) -> ArchiveChatMarkers = mod_mam_params:archive_chat_markers(?MODULE, HostType), erlang:apply(M, is_archivable_message, [?MODULE, Dir, Packet, ArchiveChatMarkers]). --spec hooks(jid:lserver()) -> [ejabberd_hooks:hook()]. -hooks(HostType) -> - [{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, - {user_send_packet, HostType, ?MODULE, user_send_packet, 60}, +-spec legacy_hooks(jid:lserver()) -> [ejabberd_hooks:hook()]. +legacy_hooks(HostType) -> + [{user_send_packet, HostType, ?MODULE, user_send_packet, 60}, {rest_user_send_packet, HostType, ?MODULE, user_send_packet, 60}, {filter_local_packet, HostType, ?MODULE, filter_packet, 60}, {remove_user, HostType, ?MODULE, remove_user, 50}, @@ -692,6 +695,9 @@ hooks(HostType) -> {get_personal_data, HostType, ?MODULE, get_personal_data, 50} | mongoose_metrics_mam_hooks:get_mam_hooks(HostType)]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + add_iq_handlers(HostType, Opts) -> Component = ejabberd_sm, %% `parallel' is the only one recommended here. diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index 6563e4da3cb..b094d3b3d9b 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -42,13 +42,13 @@ process_sm_iq/5, disco_local_items/1, disco_local_identity/1, - disco_local_features/1, + disco_local_features/3, disco_sm_items/1, disco_sm_identity/1, disco_sm_features/1, ping_command/4]). --ignore_xref([disco_local_features/1, disco_local_identity/1, disco_local_items/1, +-ignore_xref([disco_local_features/3, disco_local_identity/1, disco_local_items/1, disco_sm_features/1, disco_sm_identity/1, disco_sm_items/1, ping_command/4, process_local_iq/5, process_sm_iq/5]). @@ -61,11 +61,13 @@ start(HostType, #{iqdisc := IQDisc}) -> [gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_COMMANDS, Component, Fn, #{}, IQDisc) || {Component, Fn} <- iq_handlers()], - ejabberd_hooks:add(hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), [gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_COMMANDS, Component) || {Component, _Fn} <- iq_handlers()], ok. @@ -74,15 +76,17 @@ iq_handlers() -> [{ejabberd_local, fun ?MODULE:process_local_iq/5}, {ejabberd_sm, fun ?MODULE:process_sm_iq/5}]. -hooks(HostType) -> +legacy_hooks(HostType) -> [{disco_local_identity, HostType, ?MODULE, disco_local_identity, 99}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 99}, {disco_local_items, HostType, ?MODULE, disco_local_items, 99}, {disco_sm_identity, HostType, ?MODULE, disco_sm_identity, 99}, {disco_sm_features, HostType, ?MODULE, disco_sm_features, 99}, {disco_sm_items, HostType, ?MODULE, disco_sm_items, 99}, {adhoc_local_commands, HostType, ?MODULE, ping_command, 100}]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + %%% %%% config_spec %%% @@ -212,17 +216,19 @@ command_list_identity(Lang) -> %%------------------------------------------------------------------------- --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_COMMANDS], Acc); -disco_local_features(Acc = #{node := ?NS_COMMANDS}) -> +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([?NS_COMMANDS], Acc)}; +disco_local_features(Acc = #{node := ?NS_COMMANDS}, _, _) -> %% override all lesser features... - Acc#{result := []}; -disco_local_features(Acc = #{node := <<"ping">>}) -> + {ok, Acc#{result := []}}; +disco_local_features(Acc = #{node := <<"ping">>}, _, _) -> %% override all lesser features... - Acc#{result := [?NS_COMMANDS]}; -disco_local_features(Acc) -> - Acc. + {ok, Acc#{result := [?NS_COMMANDS]}}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. %%------------------------------------------------------------------------- diff --git a/src/mod_amp.erl b/src/mod_amp.erl index e6fb5cae59a..ac0e65b3324 100644 --- a/src/mod_amp.erl +++ b/src/mod_amp.erl @@ -11,11 +11,11 @@ -export([start/2, stop/1, supported_features/0]). -export([run_initial_check/2, check_packet/2, - disco_local_features/1, + disco_local_features/3, c2s_stream_features/3 ]). --ignore_xref([c2s_stream_features/3, disco_local_features/1, run_initial_check/2]). +-ignore_xref([c2s_stream_features/3, disco_local_features/3, run_initial_check/2]). -include("amp.hrl"). -include("mongoose.hrl"). @@ -29,23 +29,27 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, _Opts) -> - ejabberd_hooks:add(hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)). + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)). -spec supported_features() -> [atom()]. supported_features() -> [dynamic_domains]. -hooks(HostType) -> +legacy_hooks(HostType) -> [{c2s_stream_features, HostType, ?MODULE, c2s_stream_features, 50}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 99}, {c2s_preprocessing_hook, HostType, ?MODULE, run_initial_check, 10}, {amp_verify_support, HostType, ?AMP_RESOLVER, verify_support, 10}, {amp_check_condition, HostType, ?AMP_RESOLVER, check_condition, 10}, {amp_determine_strategy, HostType, ?AMP_STRATEGY, determine_strategy, 10}]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + %% API -spec run_initial_check(mongoose_acc:t(), ejabberd_c2s:state()) -> mongoose_acc:t(). @@ -64,12 +68,15 @@ check_packet(Acc, Event) -> Rules -> process_event(Acc, Rules, Event) end. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := Node}) -> - case amp_features(Node) of +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := Node}, _, _) -> + NewAcc = case amp_features(Node) of [] -> Acc; Features -> mongoose_disco:add_features(Features, Acc) - end. + end, + {ok, NewAcc}. -spec c2s_stream_features([exml:element()], mongooseim:host_type(), jid:lserver()) -> [exml:element()]. diff --git a/src/mod_auth_token.erl b/src/mod_auth_token.erl index 845a382485f..18c614d23a0 100644 --- a/src/mod_auth_token.erl +++ b/src/mod_auth_token.erl @@ -17,7 +17,7 @@ %% Hook handlers -export([clean_tokens/3, - disco_local_features/1]). + disco_local_features/3]). %% gen_iq_handler handlers -export([process_iq/5]). @@ -50,7 +50,7 @@ -ignore_xref([ behaviour_info/1, clean_tokens/3, datetime_to_seconds/1, deserialize/1, - disco_local_features/1, expiry_datetime/3, get_key_for_host_type/2, process_iq/5, + disco_local_features/3, expiry_datetime/3, get_key_for_host_type/2, process_iq/5, revoke/2, revoke_token_command/1, seconds_to_datetime/1, serialize/1, token/3, token_with_mac/2 ]). @@ -78,7 +78,8 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, #{iqdisc := IQDisc} = Opts) -> mod_auth_token_backend:start(HostType, Opts), - ejabberd_hooks:add(hooks(HostType)), + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), gen_iq_handler:add_iq_handler_for_domain( HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm, fun ?MODULE:process_iq/5, #{}, IQDisc), @@ -88,12 +89,15 @@ start(HostType, #{iqdisc := IQDisc} = Opts) -> -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm), - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), ok. +legacy_hooks(HostType) -> + [{remove_user, HostType, ?MODULE, clean_tokens, 50}]. + hooks(HostType) -> - [{remove_user, HostType, ?MODULE, clean_tokens, 50}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 90}]. + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 90}]. -spec supported_features() -> [atom()]. supported_features() -> @@ -446,8 +450,10 @@ clean_tokens(Acc, User, Server) -> config_metrics(HostType) -> mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_ESL_TOKEN_AUTH], Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([?NS_ESL_TOKEN_AUTH], Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 766db27a4c3..23571afb4d9 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -14,10 +14,10 @@ -export([ process_iq_get/5, process_iq_set/4, - disco_local_features/1 + disco_local_features/3 ]). --ignore_xref([disco_local_features/1, process_iq_get/5, process_iq_set/4]). +-ignore_xref([disco_local_features/3, process_iq_get/5, process_iq_set/4]). -include("jlib.hrl"). -include("mod_privacy.hrl"). @@ -26,11 +26,13 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) when is_map(Opts) -> - ejabberd_hooks:add(hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)). + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)). deps(_HostType, Opts) -> [{mod_privacy, Opts, hard}]. @@ -42,16 +44,20 @@ supported_features() -> config_spec() -> mod_privacy:config_spec(). -hooks(HostType) -> - [{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, - {privacy_iq_get, HostType, ?MODULE, process_iq_get, 50}, +legacy_hooks(HostType) -> + [{privacy_iq_get, HostType, ?MODULE, process_iq_get, 50}, {privacy_iq_set, HostType, ?MODULE, process_iq_set, 50}]. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_BLOCKING], Acc); -disco_local_features(Acc) -> - Acc. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([?NS_BLOCKING], Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. process_iq_get(Acc, _From = #jid{luser = LUser, lserver = LServer}, _, #iq{xmlns = ?NS_BLOCKING}, _) -> diff --git a/src/mod_caps.erl b/src/mod_caps.erl index c91b44e7355..8a0c5a63495 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -36,7 +36,7 @@ -behaviour(mongoose_module_metrics). -export([read_caps/1, caps_stream_features/3, - disco_local_features/1, disco_local_identity/1, disco_info/1]). + disco_local_features/3, disco_local_identity/1, disco_info/1]). %% gen_mod callbacks -export([start/2, start_link/2, stop/1, config_spec/0, supported_features/0]). @@ -53,7 +53,7 @@ -export([delete_caps/1, make_disco_hash/2]). -ignore_xref([c2s_broadcast_recipients/5, c2s_filter_packet/5, c2s_presence_in/4, - caps_stream_features/3, delete_caps/1, disco_info/1, disco_local_features/1, + caps_stream_features/3, delete_caps/1, disco_info/1, disco_local_features/3, disco_local_identity/1, make_disco_hash/2, read_caps/1, start_link/2, user_receive_packet/5, user_send_packet/4]). @@ -225,12 +225,15 @@ caps_stream_features(Acc, HostType, LServer) -> | Acc] end. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := Node}) -> - case is_valid_node(Node) of +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := Node}, _, _) -> + NewAcc = case is_valid_node(Node) of true -> Acc#{node := <<>>}; false -> Acc - end. + end, + {ok, NewAcc}. -spec disco_local_identity(mongoose_disco:identity_acc()) -> mongoose_disco:identity_acc(). disco_local_identity(Acc = #{node := Node}) -> @@ -357,7 +360,8 @@ init_db(mnesia) -> init([HostType, #{cache_size := MaxSize, cache_life_time := LifeTime}]) -> init_db(db_type(HostType)), cache_tab:new(caps_features, [{max_size, MaxSize}, {life_time, LifeTime}]), - ejabberd_hooks:add(hooks(HostType)), + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), {ok, #state{host_type = HostType}}. -spec handle_call(term(), any(), state()) -> @@ -375,9 +379,10 @@ handle_info(_Info, State) -> {noreply, State}. -spec terminate(any(), state()) -> ok. terminate(_Reason, #state{host_type = HostType}) -> - ejabberd_hooks:delete(hooks(HostType)). + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)). -hooks(HostType) -> +legacy_hooks(HostType) -> [{c2s_presence_in, HostType, ?MODULE, c2s_presence_in, 75}, {c2s_filter_packet, HostType, ?MODULE, c2s_filter_packet, 75}, {c2s_broadcast_recipients, HostType, ?MODULE, c2s_broadcast_recipients, 75}, @@ -385,10 +390,12 @@ hooks(HostType) -> {user_receive_packet, HostType, ?MODULE, user_receive_packet, 75}, {c2s_stream_features, HostType, ?MODULE, caps_stream_features, 75}, {s2s_stream_features, HostType, ?MODULE, caps_stream_features, 75}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 1}, {disco_local_identity, HostType, ?MODULE, disco_local_identity, 1}, {disco_info, HostType, ?MODULE, disco_info, 1}]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 1}]. + -spec code_change(any(), state(), any()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 1547c495cec..2d747c3cb8c 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -38,7 +38,7 @@ is_carbon_copy/1]). %% Hooks --export([disco_local_features/1, +-export([disco_local_features/3, user_send_packet/4, user_receive_packet/5, iq_handler2/5, @@ -49,7 +49,7 @@ %% Tests -export([should_forward/3]). --ignore_xref([disco_local_features/1, is_carbon_copy/1, remove_connection/5, +-ignore_xref([disco_local_features/3, is_carbon_copy/1, remove_connection/5, should_forward/3, user_receive_packet/5, user_send_packet/4]). -define(CC_KEY, 'cc'). @@ -77,34 +77,41 @@ is_carbon_copy(Packet) -> %% Default IQDisc is no_queue: %% executes disable/enable actions in the c2s process itself start(HostType, #{iqdisc := IQDisc}) -> - ejabberd_hooks:add(hooks(HostType)), + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_CC_2, ejabberd_sm, fun ?MODULE:iq_handler2/5, #{}, IQDisc), gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_CC_1, ejabberd_sm, fun ?MODULE:iq_handler1/5, #{}, IQDisc). stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_CC_1, ejabberd_sm), gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_CC_2, ejabberd_sm), ok. -hooks(HostType) -> - [{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, - {unset_presence_hook, HostType, ?MODULE, remove_connection, 10}, +legacy_hooks(HostType) -> + [{unset_presence_hook, HostType, ?MODULE, remove_connection, 10}, {user_send_packet, HostType, ?MODULE, user_send_packet, 89}, {user_receive_packet, HostType, ?MODULE, user_receive_packet, 89}]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}]. + -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> #section{items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc()}, defaults = #{<<"iqdisc">> => no_queue}}. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_CC_1, ?NS_CC_2, ?NS_CC_RULES], Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + NewAcc = mongoose_disco:add_features([?NS_CC_1, ?NS_CC_2, ?NS_CC_RULES], Acc), + {ok, NewAcc}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. iq_handler2(Acc, From, _To, IQ, _Extra) -> iq_handler(Acc, From, IQ, ?NS_CC_2). diff --git a/src/mod_disco.erl b/src/mod_disco.erl index c362b7e8cf5..fc96d018c5f 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -47,10 +47,10 @@ disco_sm_identity/1, disco_local_items/1, disco_sm_items/1, - disco_local_features/1, + disco_local_features/3, disco_info/1]). --ignore_xref([disco_info/1, disco_local_features/1, disco_local_identity/1, +-ignore_xref([disco_info/1, disco_local_features/3, disco_local_identity/1, disco_local_items/1, disco_sm_identity/1, disco_sm_items/1]). -include("mongoose.hrl"). @@ -64,23 +64,27 @@ start(HostType, #{iqdisc := IQDisc}) -> [gen_iq_handler:add_iq_handler_for_domain(HostType, NS, Component, Handler, #{}, IQDisc) || {Component, NS, Handler} <- iq_handlers()], - ejabberd_hooks:add(hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)), [gen_iq_handler:remove_iq_handler_for_domain(HostType, NS, Component) || {Component, NS, _Handler} <- iq_handlers()], ok. -hooks(HostType) -> +legacy_hooks(HostType) -> [{disco_local_items, HostType, ?MODULE, disco_local_items, 100}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 100}, {disco_local_identity, HostType, ?MODULE, disco_local_identity, 100}, {disco_sm_items, HostType, ?MODULE, disco_sm_items, 100}, {disco_sm_identity, HostType, ?MODULE, disco_sm_identity, 100}, {disco_info, HostType, ?MODULE, disco_info, 100}]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 100}]. + iq_handlers() -> [{ejabberd_local, ?NS_DISCO_ITEMS, fun ?MODULE:process_local_iq_items/5}, {ejabberd_local, ?NS_DISCO_INFO, fun ?MODULE:process_local_iq_info/5}, @@ -227,11 +231,13 @@ disco_sm_items(Acc = #{to_jid := To, node := <<>>}) -> disco_sm_items(Acc) -> Acc. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([<<"iq">>, <<"presence">>, <<"presence-invisible">>], Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([<<"iq">>, <<"presence">>, <<"presence-invisible">>], Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. %% @doc Support for: XEP-0157 Contact Addresses for XMPP Services -spec disco_info(mongoose_disco:info_acc()) -> mongoose_disco:info_acc(). diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index c60646178f9..959588183ec 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -1315,7 +1315,7 @@ disco_sm_items(Acc = #{host_type := HostType}) -> %%% offered by the server. -spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). disco_local_features(Acc = #{host_type := HostType}) -> - run_hook_for_host_type(disco_local_features, HostType, Acc, []). + run_hook_for_host_type(disco_local_features, HostType, Acc, #{}). %%% @doc `disco_sm_features' hook is called to get the features of the client %%% when a discovery IQ gets to session management. diff --git a/src/privacy/mod_privacy.erl b/src/privacy/mod_privacy.erl index 22dd3f5b9c2..59116227827 100644 --- a/src/privacy/mod_privacy.erl +++ b/src/privacy/mod_privacy.erl @@ -43,7 +43,7 @@ remove_user/3, remove_domain/3, updated_list/3, - disco_local_features/1, + disco_local_features/3, remove_unused_backend_opts/1 ]). @@ -52,7 +52,7 @@ -ignore_xref([ behaviour_info/1, check_packet/5, get_user_list/3, process_iq_get/5, process_iq_set/4, remove_user/3, updated_list/3, - remove_user/3, remove_domain/3, disco_local_features/1]). + remove_user/3, remove_domain/3, disco_local_features/3]). -include("jlib.hrl"). -include("mod_privacy.hrl"). @@ -73,11 +73,13 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) when is_map(Opts) -> mod_privacy_backend:init(HostType, Opts), - ejabberd_hooks:add(hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)). + ejabberd_hooks:delete(legacy_hooks(HostType)), + gen_hook:delete_handlers(hooks(HostType)). config_spec() -> #section{ @@ -110,9 +112,8 @@ remove_unused_backend_opts(Opts) -> maps:remove(riak, Opts). supported_features() -> [dynamic_domains]. -hooks(HostType) -> +legacy_hooks(HostType) -> [ - {disco_local_features, HostType, ?MODULE, disco_local_features, 98}, {privacy_iq_get, HostType, ?MODULE, process_iq_get, 50}, {privacy_iq_set, HostType, ?MODULE, process_iq_set, 50}, {privacy_get_user_list, HostType, ?MODULE, get_user_list, 50}, @@ -123,15 +124,20 @@ hooks(HostType) -> {anonymous_purge_hook, HostType, ?MODULE, remove_user, 50} ]. +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 98}]. + %% ------------------------------------------------------------------ %% Handlers %% ------------------------------------------------------------------ --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{node := <<>>}) -> - mongoose_disco:add_features([?NS_PRIVACY], Acc); -disco_local_features(Acc) -> - Acc. +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{node := <<>>}, _, _) -> + {ok, mongoose_disco:add_features([?NS_PRIVACY], Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. process_iq_get(Acc, _From = #jid{luser = LUser, lserver = LServer}, diff --git a/src/pubsub/mod_pubsub.erl b/src/pubsub/mod_pubsub.erl index aada409c59a..10bc029b9d2 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -69,7 +69,7 @@ -export([presence_probe/4, caps_recognised/4, in_subscription/5, out_subscription/4, on_user_offline/5, remove_user/3, - disco_local_features/1, + disco_local_features/3, disco_sm_identity/1, disco_sm_features/1, disco_sm_items/1, handle_pep_authorization_response/1, handle_remote_hook/4]). @@ -120,7 +120,7 @@ {?MOD_PUBSUB_DB_BACKEND, set_subscription_opts, 4}, {?MOD_PUBSUB_DB_BACKEND, stop, 0}, affiliation_to_string/1, caps_recognised/4, create_node/7, default_host/0, - delete_item/4, delete_node/3, disco_local_features/1, disco_sm_features/1, + delete_item/4, delete_node/3, disco_local_features/3, disco_sm_features/1, disco_sm_identity/1, disco_sm_items/1, extended_error/3, get_cached_item/2, get_item/3, get_items/2, get_personal_data/3, handle_pep_authorization_response/1, handle_remote_hook/4, host/2, in_subscription/5, iq_sm/4, node_action/4, node_call/4, @@ -398,7 +398,8 @@ init([ServerHost, Opts = #{host := SubdomainPattern}]) -> init_backend(ServerHost, Opts), Plugins = init_plugins(Host, ServerHost, Opts), - add_hooks(ServerHost, hooks()), + add_hooks(ServerHost, legacy_hooks()), + gen_hook:add_handlers(hooks(ServerHost)), case lists:member(?PEPNODE, Plugins) of true -> add_hooks(ServerHost, pep_hooks()), @@ -435,10 +436,9 @@ add_hooks(ServerHost, Hooks) -> delete_hooks(ServerHost, Hooks) -> [ ejabberd_hooks:delete(Hook, ServerHost, ?MODULE, F, Seq) || {Hook, F, Seq} <- Hooks ]. -hooks() -> +legacy_hooks() -> [ {sm_remove_connection_hook, on_user_offline, 75}, - {disco_local_features, disco_local_features, 75}, {presence_probe_hook, presence_probe, 80}, {roster_in_subscription, in_subscription, 50}, {roster_out_subscription, out_subscription, 50}, @@ -447,6 +447,9 @@ hooks() -> {get_personal_data, get_personal_data, 50} ]. +hooks(ServerHost) -> + [{disco_local_features, ServerHost, fun ?MODULE:disco_local_features/3, #{}, 75}]. + pep_hooks() -> [ {caps_recognised, caps_recognised, 80}, @@ -616,12 +619,15 @@ node_identity(Host, Type) -> false -> [] end. --spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). -disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}) -> + +-spec disco_local_features(mongoose_disco:feature_acc(), + map(), + map()) -> {ok, mongoose_disco:feature_acc()}. +disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}, _, _) -> Features = [?NS_PUBSUB | [feature(F) || F <- features(LServer, <<>>)]], - mongoose_disco:add_features(Features, Acc); -disco_local_features(Acc) -> - Acc. + {ok, mongoose_disco:add_features(Features, Acc)}; +disco_local_features(Acc, _, _) -> + {ok, Acc}. -spec disco_sm_identity(mongoose_disco:identity_acc()) -> mongoose_disco:identity_acc(). disco_sm_identity(Acc = #{from_jid := From, to_jid := To, node := Node}) -> @@ -928,7 +934,8 @@ terminate(_Reason, #state{host = Host, server_host = ServerHost, delete_pep_iq_handlers(ServerHost); false -> ok end, - delete_hooks(ServerHost, hooks()), + delete_hooks(ServerHost, legacy_hooks()), + gen_hook:delete_handlers(hooks(ServerHost)), case whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) of undefined -> ?LOG_ERROR(#{what => pubsub_process_is_dead, From 8a52cfb09a8ee87dffb85283f364b6d198c83e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:28:43 +0200 Subject: [PATCH 013/117] Add mongoose_admin_api This module will delegate Admin REST API calls to the handler modules, just like mongoose_client_api is doing for client API requests. --- src/mongoose_admin_api/mongoose_admin_api.erl | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/mongoose_admin_api/mongoose_admin_api.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl new file mode 100644 index 00000000000..bf089545643 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -0,0 +1,142 @@ +-module(mongoose_admin_api). + +-behaviour(mongoose_http_handler). + +%% mongoose_http_handler callbacks +-export([config_spec/0, routes/1]). + +%% config processing callbacks +-export([process_config/1]). + +%% Utilities for the handler modules +-export([init/2, + is_authorized/2, + parse_body/1, + parse_qs/1, + try_handle_request/3, + throw_error/2, + resource_created/4, + respond/3]). + +-include("mongoose_config_spec.hrl"). + +-type handler_options() :: #{path := string(), username => binary(), password => binary(), + atom() => any()}. +-type req() :: cowboy_req:req(). +-type state() :: map(). + +%% mongoose_http_handler callbacks + +-spec config_spec() -> mongoose_config_spec:config_section(). +config_spec() -> + #section{items = #{<<"username">> => #option{type = binary}, + <<"password">> => #option{type = binary}}, + process = fun ?MODULE:process_config/1}. + +-spec process_config(handler_options()) -> handler_options(). +process_config(Opts) -> + case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of + true -> + Opts; + false -> + error(#{what => both_username_and_password_required, opts => Opts}) + end. + +-spec routes(handler_options()) -> mongoose_http_handler:routes(). +routes(Opts = #{path := BasePath}) -> + [{[BasePath, Path], Module, ModuleOpts} + || {Path, Module, ModuleOpts} <- api_paths(Opts)]. + +api_paths(Opts) -> + [{"/contacts/:user/[:contact]", mongoose_admin_api_contacts, Opts}, + {"/contacts/:user/:contact/manage", mongoose_admin_api_contacts, Opts#{suffix => manage}}, + {"/users/:domain/[:username]", mongoose_admin_api_users, Opts}, + {"/sessions/:domain/[:username]/[:resource]", mongoose_admin_api_sessions, Opts}, + {"/messages/:owner/:with", mongoose_admin_api_messages, Opts}, + {"/messages/[:owner]", mongoose_admin_api_messages, Opts}, + {"/stanzas", mongoose_admin_api_stanzas, Opts}, + {"/muc-lights/:domain", mongoose_admin_api_muc_light, Opts}, + {"/muc-lights/:domain/:id/participants", mongoose_admin_api_muc_light, + Opts#{suffix => participants}}, + {"/muc-lights/:domain/:id/messages", mongoose_admin_api_muc_light, + Opts#{suffix => messages}}, + {"/muc-lights/:domain/:id/management", mongoose_admin_api_muc_light, + Opts#{suffix => management}}, + {"/mucs/:domain", mongoose_admin_api_muc, Opts}, + {"/mucs/:domain/:name/:arg", mongoose_admin_api_muc, Opts}, + {"/inbox/:host_type/:days/bin", mongoose_admin_api_inbox, Opts}, + {"/inbox/:domain/:user/:days/bin", mongoose_admin_api_inbox, Opts} + ]. + +%% Utilities for the handler modules + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, State) -> + {cowboy_rest, set_cors_headers(Req), State}. + +set_cors_headers(Req) -> + Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>, + <<"GET, OPTIONS, PUT, POST, DELETE">>, Req), + Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, + <<"*">>, Req1), + cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>, + <<"Content-Type">>, Req2). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + AuthDetails = mongoose_api_common:get_auth_details(Req), + case authorize(State, AuthDetails) of + true -> + {true, Req, State}; + false -> + mongoose_api_common:make_unauthorized_response(Req, State) + end. + +authorize(#{username := Username, password := Password}, AuthDetails) -> + case AuthDetails of + {AuthMethod, Username, Password} -> + mongoose_api_common:is_known_auth_method(AuthMethod); + _ -> + false + end; +authorize(#{}, _) -> + true. % no credentials required + +-spec parse_body(req()) -> jiffy:json_value(). +parse_body(Req) -> + try + {Params, _} = mongoose_api_common:parse_request_body(Req), + maps:from_list(Params) + catch _:_ -> + throw_error(bad_request, <<"Invalid request body">>) + end. + +-spec parse_qs(req()) -> #{atom() => binary()}. +parse_qs(Req) -> + maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]). + +-spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result. +try_handle_request(Req, State, F) -> + try + F(Req, State) + catch throw:#{error_type := ErrorType, message := Msg} -> + mongoose_api_common:error_response(ErrorType, Msg, Req, State) + end. + +-spec throw_error(atom(), iodata()) -> no_return(). +throw_error(ErrorType, Msg) -> + throw(#{error_type => ErrorType, message => Msg}). + +-spec resource_created(req(), state(), iodata(), iodata()) -> {stop, req(), state()}. +resource_created(Req, State, Path, Body) -> + Req2 = cowboy_req:set_resp_body(Body, Req), + Headers = #{<<"location">> => Path}, + Req3 = cowboy_req:reply(201, Headers, Req2), + {stop, Req3, State}. + +%% @doc Send response when it can't be returned in a tuple from the handler (e.g. for DELETE) +-spec respond(req(), state(), jiffy:json_value()) -> {stop, req(), state()}. +respond(Req, State, Response) -> + Req2 = cowboy_req:set_resp_body(jiffy:encode(Response), Req), + Req3 = cowboy_req:reply(200, Req2), + {stop, Req3, State}. From d754a604ad11371f1459ee02412a134710c7d157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:30:47 +0200 Subject: [PATCH 014/117] Add Admin API module for contacts (roster) Also: fix types in mod_roster_api --- src/mod_roster_api.erl | 5 +- .../mongoose_admin_api_contacts.erl | 160 ++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_contacts.erl diff --git a/src/mod_roster_api.erl b/src/mod_roster_api.erl index cd38d588906..ddcf027f576 100644 --- a/src/mod_roster_api.erl +++ b/src/mod_roster_api.erl @@ -113,7 +113,8 @@ subscription(#jid{lserver = LServer} = CallerJID, ContactJID, Type) -> ?UNKNOWN_DOMAIN_RESULT end. --spec set_mutual_subscription(jid:jid(), jid:jid(), sub_mutual_action()) -> {ok, iolist()}. +-spec set_mutual_subscription(jid:jid(), jid:jid(), sub_mutual_action()) -> + {ok | contact_not_found | internal | unknown_domain | user_not_exist, iolist()}. set_mutual_subscription(UserA, UserB, connect) -> subscribe_both({UserA, <<>>, []}, {UserB, <<>>, []}); set_mutual_subscription(UserA, UserB, disconnect) -> @@ -127,7 +128,7 @@ set_mutual_subscription(UserA, UserB, disconnect) -> end. -spec subscribe_both({jid:jid(), binary(), [binary()]}, {jid:jid(), binary(), [binary()]}) -> - {ok, iolist()}. + {ok | internal | unknown_domain | user_not_exist, iolist()}. subscribe_both({UserA, NameA, GroupsA}, {UserB, NameB, GroupsB}) -> Seq = [fun() -> add_contact(UserA, UserB, NameB, GroupsB) end, fun() -> add_contact(UserB, UserA, NameA, GroupsA) end, diff --git a/src/mongoose_admin_api/mongoose_admin_api_contacts.erl b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl new file mode 100644 index 00000000000..8d8a05f6dd3 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl @@ -0,0 +1,160 @@ +-module(mongoose_admin_api_contacts). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "POST" or "PUT" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + F = case cowboy_req:method(Req) of + <<"POST">> -> fun handle_post/2; + <<"PUT">> -> fun handle_put/2 + end, + try_handle_request(Req, State, F). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_get(Req, State) -> + Bindings = cowboy_req:bindings(Req), + UserJid = get_user_jid(Bindings), + case mod_roster_api:list_contacts(UserJid) of + {ok, Rosters} -> + {jiffy:encode(lists:map(fun roster_info/1, Rosters)), Req, State}; + {unknown_domain, Reason} -> + throw_error(not_found, Reason) + end. + +handle_post(Req, State) -> + Bindings = cowboy_req:bindings(Req), + UserJid = get_user_jid(Bindings), + Args = parse_body(Req), + ContactJid = get_jid(Args), + case mod_roster_api:add_contact(UserJid, ContactJid, <<>>, []) of + {unknown_domain, Reason} -> + throw_error(not_found, Reason); + {user_not_exist, Reason} -> + throw_error(not_found, Reason); + {ok, _} -> + {true, Req, State} + end. + +handle_put(Req, State) -> + Bindings = cowboy_req:bindings(Req), + UserJid = get_user_jid(Bindings), + ContactJid = get_contact_jid(Bindings), + Args = parse_body(Req), + Action = get_action(Args, State), + case perform_action(UserJid, ContactJid, Action, State) of + {unknown_domain, Reason} -> + throw_error(not_found, Reason); + {user_not_exist, Reason} -> + throw_error(not_found, Reason); + {contact_not_found, Reason} -> + throw_error(not_found, Reason); + {ok, _} -> + {true, Req, State} + end. + +handle_delete(Req, State) -> + Bindings = cowboy_req:bindings(Req), + UserJid = get_user_jid(Bindings), + ContactJid = get_contact_jid(Bindings), + case mod_roster_api:delete_contact(UserJid, ContactJid) of + {contact_not_found, Reason} -> + throw_error(not_found, Reason); + {ok, _} -> + {true, Req, State} + end. + +perform_action(UserJid, ContactJid, Action, #{suffix := manage}) -> + mod_roster_api:set_mutual_subscription(UserJid, ContactJid, Action); +perform_action(UserJid, ContactJid, Action, #{}) -> + mod_roster_api:subscription(UserJid, ContactJid, Action). + +-spec roster_info(mod_roster:roster()) -> jiffy:json_object(). +roster_info(Roster) -> + #{jid := Jid, subscription := Sub, ask := Ask} = mod_roster:item_to_map(Roster), + #{jid => jid:to_binary(Jid), subscription => Sub, ask => Ask}. + +get_jid(#{jid := JidBin}) -> + case jid:from_binary(JidBin) of + error -> throw_error(bad_request, <<"Invalid JID">>); + Jid -> Jid + end; +get_jid(#{}) -> + throw_error(bad_request, <<"Missing JID">>). + +get_user_jid(#{user := User}) -> + case jid:from_binary(User) of + error -> throw_error(bad_request, <<"Invalid user JID">>); + Jid -> Jid + end. + +get_contact_jid(#{contact := Contact}) -> + case jid:from_binary(Contact) of + error -> throw_error(bad_request, <<"Invalid contact JID">>); + Jid -> Jid + end; +get_contact_jid(#{}) -> + throw_error(bad_request, <<"Missing contact JID">>). + +get_action(#{action := ActionBin}, State) -> + decode_action(ActionBin, maps:get(suffix, State, no_suffix)); +get_action(#{}, _State) -> + throw_error(bad_request, <<"Missing action">>). + +decode_action(<<"subscribe">>, no_suffix) -> subscribe; +decode_action(<<"subscribed">>, no_suffix) -> subscribed; +decode_action(<<"connect">>, manage) -> connect; +decode_action(<<"disconnect">>, manage) -> disconnect; +decode_action(_, _) -> throw_error(bad_request, <<"Invalid action">>). From 5d471f874666765ff56cea7ae2953e64c98b5e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:32:43 +0200 Subject: [PATCH 015/117] Add Admin API modules for stanzas and messages Also: fix types in mongoose_stanza_api Marked missing error handling as TO DO - it will be done in a separate task. --- .../mongoose_admin_api_messages.erl | 142 ++++++++++++++++++ .../mongoose_admin_api_stanzas.erl | 86 +++++++++++ src/mongoose_stanza_api.erl | 5 +- 3 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_messages.erl create mode 100644 src/mongoose_admin_api/mongoose_admin_api_stanzas.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api_messages.erl b/src/mongoose_admin_api/mongoose_admin_api_messages.erl new file mode 100644 index 00000000000..8bf50fae13e --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_messages.erl @@ -0,0 +1,142 @@ +-module(mongoose_admin_api_messages). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, parse_qs/1, try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"GET">>, <<"POST">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + try_handle_request(Req, State, fun handle_post/2). + +%% Internal functions + +handle_get(Req, State) -> + Bindings = cowboy_req:bindings(Req), + OwnerJid = get_owner_jid(Bindings), + WithJid = get_with_jid(Bindings), + Args = parse_qs(Req), + Limit = get_limit(Args), + Before = get_before(Args), + Rows = mongoose_stanza_api:lookup_recent_messages(OwnerJid, WithJid, Before, Limit), + Messages = lists:map(fun row_to_map/1, Rows), + {jiffy:encode(Messages), Req, State}. + +handle_post(Req, State) -> + Args = parse_body(Req), + From = get_caller(Args), + To = get_to(Args), + Body = get_body(Args), + Packet = mongoose_stanza_helper:build_message( + jid:to_binary(From), jid:to_binary(To), Body), + case mongoose_stanza_helper:route(From, To, Packet, true) of + {error, #{what := unknown_domain}} -> + throw_error(bad_request, <<"Unknown domain">>); + {error, #{what := unknown_user}} -> + throw_error(bad_request, <<"Unknown user">>); + {ok, _} -> + {true, Req, State} + end. + +-spec row_to_map(mod_mam:message_row()) -> map(). +row_to_map(#{id := Id, jid := From, packet := Msg}) -> + Jbin = jid:to_binary(From), + {Msec, _} = mod_mam_utils:decode_compact_uuid(Id), + MsgId = case xml:get_tag_attr(<<"id">>, Msg) of + {value, MId} -> MId; + false -> <<"">> + end, + Body = exml_query:path(Msg, [{element, <<"body">>}, cdata]), + #{sender => Jbin, timestamp => round(Msec / 1000000), message_id => MsgId, body => Body}. + +get_limit(#{limit := LimitBin}) -> + try + Limit = binary_to_integer(LimitBin), + true = Limit >= 0 andalso Limit =< 500, + Limit + catch + _:_ -> throw_error(bad_request, <<"Invalid limit">>) + end; +get_limit(#{}) -> 100. + +get_before(#{before := BeforeBin}) -> + try + 1000000 * binary_to_integer(BeforeBin) + catch + _:_ -> throw_error(bad_request, <<"Invalid value of 'before'">>) + end; +get_before(#{}) -> 0. + +get_owner_jid(#{owner := Owner}) -> + case jid:from_binary(Owner) of + error -> throw_error(bad_request, <<"Invalid owner JID">>); + OwnerJid -> OwnerJid + end; +get_owner_jid(#{}) -> throw_error(not_found, <<"Missing owner JID">>). + +get_with_jid(#{with := With}) -> + case jid:from_binary(With) of + error -> throw_error(bad_request, <<"Invalid interlocutor JID">>); + WithJid -> WithJid + end; +get_with_jid(#{}) -> undefined. + +get_caller(#{caller := Caller}) -> + case jid:from_binary(Caller) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + CallerJid -> CallerJid + end; +get_caller(#{}) -> throw_error(bad_request, <<"Missing sender JID">>). + +get_to(#{to := To}) -> + case jid:from_binary(To) of + error -> throw_error(bad_request, <<"Invalid recipient JID">>); + ToJid -> ToJid + end; +get_to(#{}) -> throw_error(bad_request, <<"Missing recipient JID">>). + +get_body(#{body := Body}) -> Body; +get_body(#{}) -> throw_error(bad_request, <<"Missing message body">>). diff --git a/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl b/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl new file mode 100644 index 00000000000..7e8af0b60f7 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl @@ -0,0 +1,86 @@ +-module(mongoose_admin_api_stanzas). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_accepted/2, + allowed_methods/2, + from_json/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"POST">>], Req, State}. + +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + try_handle_request(Req, State, fun handle_post/2). + +%% Internal functions + +handle_post(Req, State) -> + Args = parse_body(Req), + Stanza = get_stanza(Args), + From = get_from_jid(Stanza), + To = get_to_jid(Stanza), + case mongoose_stanza_helper:route(From, To, Stanza, true) of + {error, #{what := unknown_domain}} -> + throw_error(bad_request, <<"Unknown domain">>); + {error, #{what := unknown_user}} -> + throw_error(bad_request, <<"Unknown user">>); + {ok, _} -> + {true, Req, State} + end. + +get_stanza(#{stanza := BinStanza}) -> + case exml:parse(BinStanza) of + {ok, Stanza} -> + Stanza; + {error, _} -> + throw_error(bad_request, <<"Malformed stanza">>) + end; +get_stanza(#{}) -> throw_error(bad_request, <<"Missing stanza">>). + +get_from_jid(Stanza) -> + case exml_query:attr(Stanza, <<"from">>) of + undefined -> + throw_error(bad_request, <<"Missing sender JID">>); + JidBin -> + case jid:from_binary(JidBin) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + Jid -> Jid + end + end. + +get_to_jid(Stanza) -> + case exml_query:attr(Stanza, <<"to">>) of + undefined -> + throw_error(bad_request, <<"Missing recipient JID">>); + JidBin -> + case jid:from_binary(JidBin) of + error -> throw_error(bad_request, <<"Invalid recipient JID">>); + Jid -> Jid + end + end. diff --git a/src/mongoose_stanza_api.erl b/src/mongoose_stanza_api.erl index 8c0774dd1ab..fa3f09108d2 100644 --- a/src/mongoose_stanza_api.erl +++ b/src/mongoose_stanza_api.erl @@ -4,6 +4,7 @@ -include("jlib.hrl"). -include("mongoose_rsm.hrl"). +%% TODO fix error handling, do not crash for non-existing users %% Before is in microseconds -spec lookup_recent_messages( ArcJID :: jid:jid(), @@ -13,10 +14,6 @@ [mod_mam:message_row()]. lookup_recent_messages(_, _, _, Limit) when Limit > 500 -> throw({error, message_limit_too_high}); -lookup_recent_messages(ArcJID, With, Before, Limit) when is_binary(ArcJID) -> - lookup_recent_messages(jid:from_binary(ArcJID), With, Before, Limit); -lookup_recent_messages(ArcJID, With, Before, Limit) when is_binary(With) -> - lookup_recent_messages(ArcJID, jid:from_binary(With), Before, Limit); lookup_recent_messages(ArcJID, WithJID, Before, Limit) -> #jid{luser = LUser, lserver = LServer} = ArcJID, {ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), From 6c5ffb7eea73fd446c8d475c880dfc7d9e890fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:34:07 +0200 Subject: [PATCH 016/117] Add Admin API module for inbox Also: fix types and whitespace in mod_inbox_api --- src/inbox/mod_inbox_api.erl | 6 +- .../mongoose_admin_api_inbox.erl | 72 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_inbox.erl diff --git a/src/inbox/mod_inbox_api.erl b/src/inbox/mod_inbox_api.erl index 32e7fc10a52..0a77d7cc2d4 100644 --- a/src/inbox/mod_inbox_api.erl +++ b/src/inbox/mod_inbox_api.erl @@ -12,7 +12,7 @@ {user_does_not_exist, io_lib:format("User ~s@~s does not exist", [User, Server])}). -spec flush_user_bin(jid:jid(), Days :: integer()) -> - {ok, integer()} | {domain_not_found, binary()}. + {ok, integer()} | {domain_not_found | user_does_not_exist, iodata()}. flush_user_bin(#jid{luser = LU, lserver = LS} = JID, Days) -> case mongoose_domain_api:get_host_type(LS) of {ok, HostType} -> @@ -33,7 +33,7 @@ flush_user_bin(#jid{luser = LU, lserver = LS} = JID, Days) -> flush_domain_bin(Domain, Days) -> LDomain = jid:nodeprep(Domain), case mongoose_domain_api:get_host_type(LDomain) of - {ok, HostType} -> + {ok, HostType} -> FromTS = days_to_timestamp(Days), Count = mod_inbox_backend:empty_domain_bin(HostType, Domain, FromTS), {ok, Count}; @@ -45,7 +45,7 @@ flush_domain_bin(Domain, Days) -> {ok, integer()} | {host_type_not_found, binary()}. flush_global_bin(HostType, Days) -> case validate_host_type(HostType) of - ok -> + ok -> FromTS = days_to_timestamp(Days), Count = mod_inbox_backend:empty_global_bin(HostType, FromTS), {ok, Count}; diff --git a/src/mongoose_admin_api/mongoose_admin_api_inbox.erl b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl new file mode 100644 index 00000000000..6a9d52d6496 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl @@ -0,0 +1,72 @@ +-module(mongoose_admin_api_inbox). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + allowed_methods/2, + delete_resource/2]). + +-import(mongoose_admin_api, [try_handle_request/3, throw_error/2, respond/3]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_delete(Req, State) -> + Bindings = cowboy_req:bindings(Req), + case Bindings of + #{host_type := _} -> + flush_global_bin(Req, State, Bindings); + _ -> + flush_user_bin(Req, State, Bindings) + end. + +flush_global_bin(Req, State, #{host_type := HostType} = Bindings) -> + Days = get_days(Bindings), + case mod_inbox_api:flush_global_bin(HostType, Days) of + {host_type_not_found, Msg} -> + throw_error(not_found, Msg); + {ok, Count} -> + respond(Req, State, Count) + end. + +flush_user_bin(Req, State, Bindings) -> + JID = get_jid(Bindings), + Days = get_days(Bindings), + case mod_inbox_api:flush_user_bin(JID, Days) of + {user_does_not_exist, Msg} -> + throw_error(not_found, Msg); + {domain_not_found, Msg} -> + throw_error(not_found, Msg); + {ok, Count} -> + respond(Req, State, Count) + end. + +get_days(#{days := DaysBin}) -> + try binary_to_integer(DaysBin) + catch _:_ -> throw_error(bad_request, <<"Invalid number of days">>) + end. + +get_jid(#{user := User, domain := Domain}) -> + case jid:make_bare(User, Domain) of + error -> throw_error(bad_request, <<"Invalid JID">>); + JID -> JID + end. From 2a49bc4d91f34f35a5ba741906c7761ee59587d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:35:22 +0200 Subject: [PATCH 017/117] Add Admin API module for MUC Also: fix types in mod_muc_api --- src/mod_muc_api.erl | 2 +- .../mongoose_admin_api_muc.erl | 159 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_muc.erl diff --git a/src/mod_muc_api.erl b/src/mod_muc_api.erl index 468280429da..2193a8fc24f 100644 --- a/src/mod_muc_api.erl +++ b/src/mod_muc_api.erl @@ -468,7 +468,7 @@ room_desc_to_map(Desc) -> #{title => Title, private => Private, users_number => Number} end. --spec verify_room(jid:jid(), jid:jid()) -> ok | {internal | not_found, term()}. +-spec verify_room(jid:jid(), jid:jid()) -> ok | {internal | room_not_found, term()}. verify_room(BareRoomJID, OwnerJID) -> case mod_muc_room:can_access_room(BareRoomJID, OwnerJID) of {ok, true} -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_muc.erl b/src/mongoose_admin_api/mongoose_admin_api_muc.erl new file mode 100644 index 00000000000..a84741aafc3 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_muc.erl @@ -0,0 +1,159 @@ +-module(mongoose_admin_api_muc). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_accepted/2, + allowed_methods/2, + from_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [try_handle_request/3, throw_error/2, parse_body/1, resource_created/4]). + +-include("jlib.hrl"). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"POST">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + try_handle_request(Req, State, fun handle_post/2). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_post(Req, State) -> + Bindings = cowboy_req:bindings(Req), + handle_post(Req, State, Bindings). + +handle_post(Req, State, #{arg := <<"participants">>} = Bindings) -> + RoomJid = get_room_jid(Bindings), + Args = parse_body(Req), + SenderJid = get_sender_jid(Args), + RecipientJid = get_recipient_jid(Args), + Reason = get_invite_reason(Args), + case mod_muc_api:invite_to_room(RoomJid, SenderJid, RecipientJid, Reason) of + {ok, _} -> + {true, Req, State}; + {room_not_found, Msg} -> + throw_error(not_found, Msg) + end; +handle_post(Req, State, #{arg := <<"messages">>} = Bindings) -> + RoomJid = get_room_jid(Bindings), + Args = parse_body(Req), + SenderJid = get_from_jid(Args), + Body = get_message_body(Args), + {ok, _} = mod_muc_api:send_message_to_room(RoomJid, SenderJid, Body), + {true, Req, State}; +handle_post(Req, State, #{domain := Domain}) -> + Args = parse_body(Req), + RoomName = get_room_name(Args), + OwnerJid = get_owner_jid(Args), + Nick = get_nick(Args), + %% TODO This check should be done in the API module to work for GraphQL as well + #jid{lserver = MUCDomain} = make_room_jid(RoomName, get_muc_domain(Domain)), + case mod_muc_api:create_instant_room(MUCDomain, RoomName, OwnerJid, Nick) of + {ok, #{title := R}} -> + Path = [cowboy_req:uri(Req), "/", R], + resource_created(Req, State, Path, R); + {user_not_found, Msg} -> + throw_error(not_found, Msg) + end. + +handle_delete(Req, State) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings), + #{arg := Nick} = Bindings, % 'name' was present, so 'arg' is present as well (see Cowboy paths) + Reason = <<"User kicked from the admin REST API">>, + case mod_muc_api:kick_user_from_room(RoomJid, Nick, Reason) of + {ok, _} -> + {true, Req, State}; + {moderator_not_found, Msg} -> + throw_error(not_found, Msg); + {room_not_found, Msg} -> + throw_error(not_found, Msg) + end. + +get_owner_jid(#{owner := Owner}) -> + case jid:binary_to_bare(Owner) of + error -> throw_error(bad_request, <<"Invalid owner JID">>); + OwnerJid -> OwnerJid + end; +get_owner_jid(#{}) -> throw_error(bad_request, <<"Missing owner JID">>). + +get_room_jid(#{domain := Domain} = Bindings) -> + MUCDomain = get_muc_domain(Domain), + RoomName = get_room_name(Bindings), + make_room_jid(RoomName, MUCDomain). + +make_room_jid(RoomName, MUCDomain) -> + try #jid{} = jid:make_bare(RoomName, MUCDomain) + catch _:_ -> throw_error(bad_request, <<"Invalid room name">>) + end. + +get_nick(#{nick := Nick}) -> Nick; +get_nick(#{}) -> throw_error(bad_request, <<"Missing nickname">>). + +get_room_name(#{name := Name}) -> Name; +get_room_name(#{}) -> throw_error(bad_request, <<"Missing room name">>). + +get_message_body(#{body := Body}) -> Body; +get_message_body(#{}) -> throw_error(bad_request, <<"Missing message body">>). + +get_invite_reason(#{reason := Reason}) -> Reason; +get_invite_reason(#{}) -> throw_error(bad_request, <<"Missing invite reason">>). + +get_from_jid(#{from := Sender}) -> + case jid:from_binary(Sender) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + SenderJid -> SenderJid + end; +get_from_jid(#{}) -> throw_error(bad_request, <<"Missing sender JID">>). + +get_sender_jid(#{sender := Sender}) -> + case jid:from_binary(Sender) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + SenderJid -> SenderJid + end; +get_sender_jid(#{}) -> throw_error(bad_request, <<"Missing sender JID">>). + +get_recipient_jid(#{recipient := Recipient}) -> + case jid:from_binary(Recipient) of + error -> throw_error(bad_request, <<"Invalid recipient JID">>); + RecipientJid -> RecipientJid + end; +get_recipient_jid(#{}) -> throw_error(bad_request, <<"Missing recipient JID">>). + +get_muc_domain(Domain) -> + try + {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), + mod_muc:server_host_to_muc_host(HostType, Domain) + catch _:_ -> + throw_error(not_found, <<"MUC domain not found">>) + end. From b5c83071c326f79b3371321ae8ee272cfd121342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:36:08 +0200 Subject: [PATCH 018/117] Add Admin API module for MUC Light Also: fix types and typos (sic!) in MUC Light modules --- .../mongoose_admin_api_muc_light.erl | 173 ++++++++++++++++++ src/muc_light/mod_muc_light_api.erl | 2 +- src/muc_light/mod_muc_light_commands.erl | 2 +- 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_muc_light.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl b/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl new file mode 100644 index 00000000000..d68599275fd --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl @@ -0,0 +1,173 @@ +-module(mongoose_admin_api_muc_light). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_accepted/2, + allowed_methods/2, + from_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [try_handle_request/3, throw_error/2, parse_body/1, resource_created/4]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "POST" and "PUT" +-spec from_json(req(), state()) -> {stop, req(), state()}. +from_json(Req, State) -> + F = case cowboy_req:method(Req) of + <<"POST">> -> fun handle_post/2; + <<"PUT">> -> fun handle_put/2 + end, + try_handle_request(Req, State, F). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_post(Req, #{suffix := participants} = State) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings), + Args = parse_body(Req), + SenderJid = get_sender_jid(Args), + RecipientJid = get_recipient_jid(Args), + case mod_muc_light_api:invite_to_room(RoomJid, SenderJid, RecipientJid) of + {ok, _} -> + {stop, Req, State}; + {muc_server_not_found, Msg} -> + throw_error(not_found, Msg); + {not_room_member, Msg} -> + throw_error(denied, Msg) + end; +handle_post(Req, #{suffix := messages} = State) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings), + Args = parse_body(Req), + SenderJid = get_from_jid(Args), + Body = get_message_body(Args), + case mod_muc_light_api:send_message(RoomJid, SenderJid, Body) of + {ok, _} -> + {stop, Req, State}; + {muc_server_not_found, Msg} -> + throw_error(not_found, Msg); + {not_room_member, Msg} -> + throw_error(denied, Msg) + end; +handle_post(Req, State) -> + #{domain := MUCDomain} = cowboy_req:bindings(Req), + Args = parse_body(Req), + OwnerJid = get_owner_jid(Args), + RoomName = get_room_name(Args), + Subject = get_room_subject(Args), + case mod_muc_light_api:create_room(MUCDomain, OwnerJid, RoomName, Subject) of + {ok, #{jid := RoomJid}} -> + RoomJidBin = jid:to_binary(RoomJid), + Path = [cowboy_req:uri(Req), "/", RoomJidBin], + resource_created(Req, State, Path, RoomJidBin); + {muc_server_not_found, Msg} -> + throw_error(not_found, Msg) + end. + +handle_put(Req, State) -> + #{domain := MUCDomain} = cowboy_req:bindings(Req), + Args = parse_body(Req), + OwnerJid = get_owner_jid(Args), + RoomName = get_room_name(Args), + RoomId = get_room_id(Args), + Subject = get_room_subject(Args), + case mod_muc_light_api:create_room(MUCDomain, RoomId, OwnerJid, RoomName, Subject) of + {ok, #{jid := RoomJid}} -> + RoomJidBin = jid:to_binary(RoomJid), + Path = [cowboy_req:uri(Req), "/", RoomJidBin], + resource_created(Req, State, Path, RoomJidBin); + {muc_server_not_found, Msg} -> + throw_error(not_found, Msg); + {already_exists, Msg} -> + throw_error(denied, Msg) + end. + +handle_delete(Req, #{suffix := management} = State) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings), + case mod_muc_light_api:delete_room(RoomJid) of + {ok, _} -> + {true, Req, State}; + {muc_server_not_found, Msg} -> + throw_error(not_found, Msg); + {room_not_found, Msg} -> + throw_error(not_found, Msg) + end; +handle_delete(_Req, _State) -> + throw_error(not_found, ""). % Cowboy returns the same for unknown paths + +get_owner_jid(#{owner := Owner}) -> + case jid:from_binary(Owner) of + error -> throw_error(bad_request, <<"Invalid owner JID">>); + OwnerJid -> OwnerJid + end; +get_owner_jid(#{}) -> throw_error(bad_request, <<"Missing owner JID">>). + +get_room_jid(#{domain := MUCDomain} = Bindings) -> + RoomId = get_room_id(Bindings), + case jid:make_bare(RoomId, MUCDomain) of + error -> throw_error(bad_request, <<"Invalid room ID or domain name">>); + RoomJid -> RoomJid + end. + +get_room_name(#{name := Name}) -> Name; +get_room_name(#{}) -> throw_error(bad_request, <<"Missing room name">>). + +get_room_id(#{id := Id}) -> Id; +get_room_id(#{}) -> throw_error(bad_request, <<"Missing room ID">>). + +get_room_subject(#{subject := Subject}) -> Subject; +get_room_subject(#{}) -> throw_error(bad_request, <<"Missing room subject">>). + +get_message_body(#{body := Body}) -> Body; +get_message_body(#{}) -> throw_error(bad_request, <<"Missing message body">>). + +get_from_jid(#{from := Sender}) -> + case jid:from_binary(Sender) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + SenderJid -> SenderJid + end; +get_from_jid(#{}) -> throw_error(bad_request, <<"Missing sender JID">>). + +get_sender_jid(#{sender := Sender}) -> + case jid:from_binary(Sender) of + error -> throw_error(bad_request, <<"Invalid sender JID">>); + SenderJid -> SenderJid + end; +get_sender_jid(#{}) -> throw_error(bad_request, <<"Missing sender JID">>). + +get_recipient_jid(#{recipient := Recipient}) -> + case jid:from_binary(Recipient) of + error -> throw_error(bad_request, <<"Invalid recipient JID">>); + RecipientJid -> RecipientJid + end; +get_recipient_jid(#{}) -> throw_error(bad_request, <<"Missing recipient JID">>). diff --git a/src/muc_light/mod_muc_light_api.erl b/src/muc_light/mod_muc_light_api.erl index 483fee4bf37..83ba0f2cbb9 100644 --- a/src/muc_light/mod_muc_light_api.erl +++ b/src/muc_light/mod_muc_light_api.erl @@ -342,7 +342,7 @@ create_room_raw(InRoomJID, CreatorJID, Options) -> {ok, RoomJID, #create{aff_users = AffUsers, raw_config = Conf}} -> {ok, make_room(RoomJID, Conf, AffUsers)}; {error, exists} -> - {already_exist, "Room already exists"}; + {already_exists, "Room already exists"}; {error, max_occupants_reached} -> {max_occupants_reached, "Max occupants number reached"}; {error, {Key, Reason}} -> diff --git a/src/muc_light/mod_muc_light_commands.erl b/src/muc_light/mod_muc_light_commands.erl index f0ad029ae33..129860b283b 100644 --- a/src/muc_light/mod_muc_light_commands.erl +++ b/src/muc_light/mod_muc_light_commands.erl @@ -196,7 +196,7 @@ change_room_config(MUCServer, RoomID, RoomName, User, Subject) -> Result = mod_muc_light_api:change_room_config(RoomJID, UserJID, Config), format_result_no_msg(Result). --spec send_message(jid:server(), jid:user(), jid:literal_jid(), jid:literal_jid()) -> +-spec send_message(jid:server(), jid:user(), jid:literal_jid(), binary()) -> ok | {error, not_found | denied, iolist()}. send_message(MUCServer, RoomID, Sender, Message) -> SenderJID = jid:from_binary(Sender), From a04f373619771b4cf32a4fc050fefeff94396478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:37:06 +0200 Subject: [PATCH 019/117] Add Admin API module for sessions --- .../mongoose_admin_api_sessions.erl | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_sessions.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api_sessions.erl b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl new file mode 100644 index 00000000000..d3e067eda27 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl @@ -0,0 +1,69 @@ +-module(mongoose_admin_api_sessions). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + allowed_methods/2, + to_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"GET">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_get(Req, State) -> + #{domain := Domain} = cowboy_req:bindings(Req), + Sessions = mongoose_session_api:list_resources(Domain), + {jiffy:encode(Sessions), Req, State}. + +handle_delete(Req, State) -> + #{domain := Domain} = Bindings = cowboy_req:bindings(Req), + UserName = get_user_name(Bindings), + Resource = get_resource(Bindings), + case mongoose_session_api:kick_session(UserName, Domain, Resource, <<"kicked">>) of + {ok, _} -> + {true, Req, State}; + {no_session, Reason} -> + throw_error(not_found, Reason) + end. + +get_user_name(#{username := UserName}) -> UserName; +get_user_name(#{}) -> throw_error(bad_request, <<"Missing user name">>). + +%% Resource is matched first, so it is not possible for it to be missing +get_resource(#{resource := Resource}) -> Resource. From 34c72b30b687c954a4e43b882d4236b10e709e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:37:25 +0200 Subject: [PATCH 020/117] Add Admin API module for users (accounts) --- .../mongoose_admin_api_users.erl | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_users.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api_users.erl b/src/mongoose_admin_api/mongoose_admin_api_users.erl new file mode 100644 index 00000000000..76b83040d82 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_users.erl @@ -0,0 +1,123 @@ +-module(mongoose_admin_api_users). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "POST" or "PUT" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + F = case cowboy_req:method(Req) of + <<"POST">> -> fun handle_post/2; + <<"PUT">> -> fun handle_put/2 + end, + try_handle_request(Req, State, F). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_get(Req, State) -> + #{domain := Domain} = cowboy_req:bindings(Req), + Users = mongoose_account_api:list_users(Domain), + {jiffy:encode(Users), Req, State}. + +handle_post(Req, State) -> + #{domain := Domain} = cowboy_req:bindings(Req), + Args = parse_body(Req), + {UserName, Password} = get_credentials(Args), + case mongoose_account_api:register_user(UserName, Domain, Password) of + {exists, Reason} -> + throw_error(denied, Reason); + {invalid_jid, Reason} -> + throw_error(bad_request, Reason); + {cannot_register, Reason} -> + throw_error(internal, Reason); + {ok, Result} -> + Path = [cowboy_req:uri(Req), "/", UserName], + resource_created(Req, State, Path, Result) + end. + +handle_put(Req, State) -> + #{domain := Domain} = Bindings = cowboy_req:bindings(Req), + UserName = get_user_name(Bindings), + Args = parse_body(Req), + Password = get_new_password(Args), + case mongoose_account_api:change_password(UserName, Domain, Password) of + {empty_password, Reason} -> + throw_error(bad_request, Reason); + {invalid_jid, Reason} -> + throw_error(bad_request, Reason); + {not_allowed, Reason} -> + throw_error(denied, Reason); + {ok, _} -> + {true, Req, State} + end. + +handle_delete(Req, State) -> + #{domain := Domain} = Bindings = cowboy_req:bindings(Req), + UserName = get_user_name(Bindings), + case mongoose_account_api:unregister_user(UserName, Domain) of + {invalid_jid, Reason} -> + throw_error(bad_request, Reason); + {not_allowed, Reason} -> + throw_error(denied, Reason); + {ok, _} -> + {true, Req, State} + end. + +get_user_name(#{username := UserName}) -> UserName; +get_user_name(#{}) -> throw_error(bad_request, <<"Missing user name">>). + +get_new_password(#{newpass := Password}) -> Password; +get_new_password(#{}) -> throw_error(bad_request, <<"Missing password">>). + +get_credentials(#{username := UserName, password := Password}) -> {UserName, Password}; +get_credentials(#{}) -> throw_error(bad_request, <<"Missing credentials">>). From a9fdeac228d038287a038028f4821a684f354e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:38:57 +0200 Subject: [PATCH 021/117] Enable mongoose_admin_api (new) instead of mongoose_api_admin (old) --- big_tests/tests/rest_helper.erl | 4 ++-- rel/files/mongooseim.toml | 2 +- src/mongoose_http_handler.erl | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/big_tests/tests/rest_helper.erl b/big_tests/tests/rest_helper.erl index 3a9ffd678ed..19bd18fd1fe 100644 --- a/big_tests/tests/rest_helper.erl +++ b/big_tests/tests/rest_helper.erl @@ -254,7 +254,7 @@ insert_creds(Opts = #{handlers := Handlers}, Creds) -> NewHandlers = [inject_creds_to_opts(Handler, Creds) || Handler <- Handlers], Opts#{handlers := NewHandlers}. -inject_creds_to_opts(Handler = #{module := mongoose_api_admin}, Creds) -> +inject_creds_to_opts(Handler = #{module := mongoose_admin_api}, Creds) -> case Creds of {UserName, Password} -> Handler#{username => UserName, password => Password}; @@ -277,7 +277,7 @@ is_roles_config(#{module := ejabberd_cowboy, handlers := Handlers}, Role) -> lists:any(fun(#{module := Module}) -> Module =:= RoleModule end, Handlers); is_roles_config(_, _) -> false. -role_to_module(admin) -> mongoose_api_admin; +role_to_module(admin) -> mongoose_admin_api; role_to_module(client) -> mongoose_client_api. mapfromlist(L) -> diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index 192ab07f72f..c5f9b33b5e1 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -56,7 +56,7 @@ transport.num_acceptors = 10 transport.max_connections = 1024 - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index f526ccc48cb..efc7cf011fb 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -85,5 +85,6 @@ configurable_handler_modules() -> mongoose_client_api, mongoose_api, mongoose_api_admin, + mongoose_admin_api, mongoose_domain_handler, mongoose_graphql_cowboy_handler]. From bffcc6c537b7cdb01b2cef349099d0e7e3a3ba72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 13:39:36 +0200 Subject: [PATCH 022/117] Update REST tests for the new API - Update expected messages - Actually test message errors for MUC (invites were tested instead) - Remove tests for listing commands and adding mongoose_commands Command listing will be possible only with GraphQL, and mongoose_commands will be removed in a follow-up PR. Also: remove test repeat --- big_tests/tests/muc_http_api_SUITE.erl | 33 +++--- big_tests/tests/rest_SUITE.erl | 137 ++++--------------------- 2 files changed, 38 insertions(+), 132 deletions(-) diff --git a/big_tests/tests/muc_http_api_SUITE.erl b/big_tests/tests/muc_http_api_SUITE.erl index 0e840925340..57bd37c3d95 100644 --- a/big_tests/tests/muc_http_api_SUITE.erl +++ b/big_tests/tests/muc_http_api_SUITE.erl @@ -37,9 +37,8 @@ all() -> {group, negative}]. groups() -> - G = [{positive, [parallel], success_response() ++ complex()}, - {negative, [parallel], failure_response()}], - ct_helper:repeat_all_until_all_ok(G). + [{positive, [parallel], success_response() ++ complex()}, + {negative, [parallel], failure_response()}]. success_response() -> [ @@ -279,28 +278,25 @@ failed_invites(Config) -> Name = set_up_room(Config, Alice), BAlice = escalus_client:short_jid(Alice), BBob = escalus_client:short_jid(Bob), - % non-existing room + % Invite to a non-existent room {{<<"404">>, _}, <<"Room not found">>} = send_invite(<<"thisroomdoesnotexist">>, BAlice, BBob), - % invite with bad jid - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, BAlice, <<"@badjid">>), - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, <<"@badjid">>, BBob), + % Invite with a bad jid + {{<<"400">>, _}, <<"Invalid recipient JID">>} = send_invite(Name, BAlice, <<"@badjid">>), + {{<<"400">>, _}, <<"Invalid sender JID">>} = send_invite(Name, <<"@badjid">>, BBob), ok end). failed_messages(Config) -> - escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> Name = set_up_room(Config, Alice), - % non-existing room + % Message to a non-existent room succeeds in the current implementation BAlice = escalus_client:short_jid(Alice), - BBob = escalus_client:short_jid(Bob), - {{<<"404">>, _}, <<"Room not found">>} = send_invite(<<"thisroomdoesnotexist">>, BAlice, BBob), - % invite with bad jid - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, BAlice, <<"@badjid">>), - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, <<"@badjid">>, BBob), + {{<<"204">>, _}, <<>>} = send_message(<<"thisroomdoesnotexist">>, BAlice), + % Message from a bad jid + {{<<"400">>, _}, <<"Invalid sender JID", _/binary>>} = send_message(Name, <<"@badjid">>), ok end). - %%-------------------------------------------------------------------- %% Ancillary (adapted from the MUC suite) %%-------------------------------------------------------------------- @@ -324,6 +320,13 @@ send_invite(RoomName, BinFrom, BinTo) -> reason => Reason}, rest_helper:post(admin, Path, Body). +send_message(RoomName, BinFrom) -> + Path = path([RoomName, "messages"]), + Message = <<"Greetings!">>, + Body = #{from => BinFrom, + body => Message}, + rest_helper:post(admin, Path, Body). + make_distinct_name(Prefix) -> {_, S, US} = os:timestamp(), L = lists:flatten([integer_to_list(S rem 100), ".", integer_to_list(US)]), diff --git a/big_tests/tests/rest_SUITE.erl b/big_tests/tests/rest_SUITE.erl index a37b7861b94..cae95324731 100644 --- a/big_tests/tests/rest_SUITE.erl +++ b/big_tests/tests/rest_SUITE.erl @@ -18,7 +18,6 @@ -compile([export_all, nowarn_export_all]). -include_lib("escalus/include/escalus.hrl"). --include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("exml/include/exml.hrl"). @@ -38,7 +37,6 @@ -define(OK, {<<"200">>, <<"OK">>}). -define(CREATED, {<<"201">>, <<"Created">>}). -define(NOCONTENT, {<<"204">>, <<"No Content">>}). --define(ERROR, {<<"500">>, _}). -define(NOT_FOUND, {<<"404">>, _}). -define(NOT_AUTHORIZED, {<<"401">>, _}). -define(FORBIDDEN, {<<"403">>, _}). @@ -48,13 +46,9 @@ %% Suite configuration %%-------------------------------------------------------------------- --define(REGISTRATION_TIMEOUT, 2). %% seconds --define(ATOMS, [name, desc, category, action, security_policy, args, result, sender]). - all() -> [ {group, admin}, - {group, dynamic_module}, {group, auth}, {group, blank_auth}, {group, roster} @@ -67,8 +61,8 @@ groups() -> {roster, [parallel], [list_contacts, befriend_and_alienate, befriend_and_alienate_auto, - invalid_roster_operations]}, - {dynamic_module, [], [stop_start_command_module]}]. + invalid_roster_operations]} + ]. auth_test_cases() -> [auth_passes_correct_creds, @@ -78,8 +72,7 @@ blank_auth_testcases() -> [auth_always_passes_blank_creds]. test_cases() -> - [commands_are_listed, - non_existent_command_returns404, + [non_existent_command_returns404, existent_command_with_missing_arguments_returns404, user_can_be_registered_and_removed, sessions_are_listed, @@ -89,8 +82,7 @@ test_cases() -> stanzas_are_sent_and_received, messages_are_archived, messages_can_be_paginated, - password_can_be_changed, - types_are_checked_separately_for_args_and_return + password_can_be_changed ]. suite() -> @@ -125,61 +117,16 @@ end_per_group(auth, _Config) -> end_per_group(_GroupName, Config) -> escalus:delete_users(Config, escalus:get_users([alice, bob, mike])). -init_per_testcase(types_are_checked_separately_for_args_and_return = CaseName, Config) -> - {Mod, Code} = rpc(dynamic_compile, from_string, [custom_module_code()]), - rpc(code, load_binary, [Mod, "mod_commands_test.erl", Code]), - Config1 = dynamic_modules:save_modules(host_type(), Config), - dynamic_modules:ensure_modules(host_type(), [{mod_commands_test, []}]), - escalus:init_per_testcase(CaseName, Config1); init_per_testcase(CaseName, Config) -> MAMTestCases = [messages_are_archived, messages_can_be_paginated], rest_helper:maybe_skip_mam_test_cases(CaseName, MAMTestCases, Config). -end_per_testcase(types_are_checked_separately_for_args_and_return = CaseName, Config) -> - dynamic_modules:restore_modules(Config), - escalus:end_per_testcase(CaseName, Config); end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). rpc(M, F, A) -> distributed_helper:rpc(distributed_helper:mim(), M, F, A). -custom_module_code() -> - "-module(mod_commands_test). - -export([start/0, stop/0, start/2, stop/1, test_arg/1, test_return/1, supported_features/0]). - start() -> mongoose_commands:register(commands()). - stop() -> mongoose_commands:unregister(commands()). - start(_,_) -> start(). - stop(_) -> stop(). - supported_features() -> [dynamic_domains]. - commands() -> - [ - [ - {name, test_arg}, - {category, <<\"test_arg\">>}, - {desc, <<\"List test_arg\">>}, - {module, mod_commands_test}, - {function, test_arg}, - {action, create}, - {args, [{arg, boolean}]}, - {result, [{msg, binary}]} - ], - [ - {name, test_return}, - {category, <<\"test_return\">>}, - {desc, <<\"List test_return\">>}, - {module, mod_commands_test}, - {function, test_return}, - {action, create}, - {args, [{arg, boolean}]}, - {result, {msg, binary}} - ] - ]. - test_arg(_) -> <<\"bleble\">>. - test_return(_) -> ok. - " -. - %%-------------------------------------------------------------------- %% Tests %%-------------------------------------------------------------------- @@ -187,37 +134,19 @@ custom_module_code() -> % Authorization auth_passes_correct_creds(_Config) -> % try to login with the same creds - {?OK, _Lcmds} = gett(admin, <<"/commands">>, {<<"ala">>, <<"makota">>}). + {?OK, _Users} = gett(admin, path("users", [domain()]), {<<"ala">>, <<"makota">>}). auth_fails_incorrect_creds(_Config) -> % try to login with different creds - {?NOT_AUTHORIZED, _} = gett(admin, <<"/commands">>, {<<"ola">>, <<"mapsa">>}). + {?NOT_AUTHORIZED, _} = gett(admin, path("users", [domain()]), {<<"ola">>, <<"mapsa">>}). auth_always_passes_blank_creds(_Config) -> % we set control creds for blank rest_helper:change_admin_creds(any), % try with any auth - {?OK, Lcmds} = gett(admin, <<"/commands">>, {<<"aaaa">>, <<"bbbb">>}), + {?OK, Users} = gett(admin, path("users", [domain()]), {<<"aaaa">>, <<"bbbb">>}), % try with no auth - {?OK, Lcmds} = gett(admin, <<"/commands">>). - -commands_are_listed(_C) -> - {?OK, Lcmds} = gett(admin, <<"/commands">>), - DecCmds = decode_maplist(Lcmds), - ListCmd = #{action => <<"read">>, method => <<"GET">>, args => #{}, - category => <<"commands">>, - desc => <<"List commands">>, - name => <<"list_methods">>, - path => <<"/commands">>}, - %% Check that path and args are listed using a command with args - RosterCmd = #{action => <<"read">>, method => <<"GET">>, - args => #{caller => <<"string">>}, - category => <<"contacts">>, - desc => <<"Get roster">>, - name => <<"list_contacts">>, - path => <<"/contacts/:caller">>}, - ?assertEqual([ListCmd], assert_inlist(#{name => <<"list_methods">>}, DecCmds)), - ?assertEqual([RosterCmd], assert_inlist(#{name => <<"list_contacts">>}, DecCmds)). + {?OK, Users} = gett(admin, path("users", [domain()])). non_existent_command_returns404(_C) -> {?NOT_FOUND, _} = gett(admin, <<"/isitthereornot">>). @@ -286,8 +215,8 @@ messages_error_handling(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_message_bin(AliceJID, <<"@noway">>), - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_message_bin(<<"@noway">>, BobJID), + {{<<"400">>, _}, <<"Invalid recipient JID">>} = send_message_bin(AliceJID, <<"@noway">>), + {{<<"400">>, _}, <<"Invalid sender JID">>} = send_message_bin(<<"@noway">>, BobJID), ok end). @@ -299,9 +228,9 @@ stanzas_are_sent_and_received(Config) -> ?assertEqual(<<"attribute">>, exml_query:attr(Res, <<"extra">>)), ?assertEqual(<<"inside the sibling">>, exml_query:path(Res, [{element, <<"sibling">>}, cdata])), Res1 = send_flawed_stanza(missing_attribute, Alice, Bob), - {?BAD_REQUEST, <<"both from and to are required">>} = Res1, + {?BAD_REQUEST, <<"Missing recipient JID">>} = Res1, Res2 = send_flawed_stanza(malformed_xml, Alice, Bob), - {?BAD_REQUEST, <<"Malformed stanza: \"expected >\"">>} = Res2, + {?BAD_REQUEST, <<"Malformed stanza">>} = Res2, ok end). @@ -537,50 +466,38 @@ invalid_roster_operations(Config) -> BobS = binary_to_list(BobJID), AlicePath = lists:flatten(["/contacts/", AliceS]), % adds them to rosters - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = post(admin, AlicePath, #{jid => <<"@invalidjid">>}), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = post(admin, "/contacts/@invalid_jid", #{jid => BobJID}), + {?BAD_REQUEST, <<"Invalid JID">>} = post(admin, AlicePath, #{jid => <<"@invalidjid">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = post(admin, "/contacts/@invalid_jid", #{jid => BobJID}), % it is idempotent {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), PutPathA = lists:flatten([AlicePath, "/@invalid_jid"]), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, PutPathA, #{action => <<"subscribe">>}), + {?BAD_REQUEST, <<"Invalid contact JID">>} = putt(admin, PutPathA, #{action => <<"subscribe">>}), PutPathB = lists:flatten(["/contacts/@invalid_jid/", BobS]), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, PutPathB, #{action => <<"subscribe">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = putt(admin, PutPathB, #{action => <<"subscribe">>}), PutPathC = lists:flatten([AlicePath, "/", BobS]), - {?BAD_REQUEST, <<"invalid action">>} = putt(admin, PutPathC, #{action => <<"something stupid">>}), + {?BAD_REQUEST, <<"Invalid action">>} = putt(admin, PutPathC, #{action => <<"something stupid">>}), ManagePath = lists:flatten(["/contacts/", AliceS, "/", BobS, "/manage" ]), - {?BAD_REQUEST, <<"invalid action">>} = putt(admin, ManagePath, #{action => <<"off with his head">>}), + {?BAD_REQUEST, <<"Invalid action">>} = putt(admin, ManagePath, #{action => <<"off with his head">>}), MangePathA = lists:flatten(["/contacts/", "@invalid", "/", BobS, "/manage" ]), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, MangePathA, #{action => <<"connect">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = putt(admin, MangePathA, #{action => <<"connect">>}), MangePathB = lists:flatten(["/contacts/", AliceS, "/", "@bzzz", "/manage" ]), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, MangePathB, #{action => <<"connect">>}), - ok - end - ). - -types_are_checked_separately_for_args_and_return(Config) -> - escalus:story( - Config, [{alice, 1}], - fun(_Alice) -> - % argument doesn't pass typecheck - {?BAD_REQUEST, _} = post(admin, "/test_arg", #{arg => 1}), - % return value doesn't pass typecheck - {?ERROR, _} = post(admin, "/test_return", #{arg => true}), + {?BAD_REQUEST, <<"Invalid contact JID">>} = putt(admin, MangePathB, #{action => <<"connect">>}), ok end ). @@ -665,20 +582,6 @@ get_messages(Me, Other, Before, Count) -> {?OK, Msgs} = gett(admin, GetPath), Msgs. -stop_start_command_module(_) -> - %% Precondition: module responsible for resource is started. If we - %% stop the module responsible for this resource then the same - %% test will fail. If we start the module responsible for this - %% resource then the same test will succeed. With the precondition - %% described above we test both transition from `started' to - %% `stopped' and from `stopped' to `started'. - {?OK, _} = gett(admin, <<"/commands">>), - {stopped, _} = dynamic_modules:stop(host_type(), mod_commands), - {?NOT_FOUND, _} = gett(admin, <<"/commands">>), - {started, _} = dynamic_modules:start(host_type(), mod_commands, []), - timer:sleep(200), %% give the server some time to build the paths again - {?OK, _} = gett(admin, <<"/commands">>). - to_list(V) when is_binary(V) -> binary_to_list(V); to_list(V) when is_list(V) -> From df90b8c57fcb51d42a69bc0311ab9df3aa6a558a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 7 Sep 2022 15:49:35 +0200 Subject: [PATCH 023/117] Cover error cases in rest_SUITE Also: reorganize tests for easier management --- big_tests/tests/rest_SUITE.erl | 319 ++++++++++++------ .../mongoose_admin_api_users.erl | 13 +- 2 files changed, 214 insertions(+), 118 deletions(-) diff --git a/big_tests/tests/rest_SUITE.erl b/big_tests/tests/rest_SUITE.erl index cae95324731..ce7c0de79e7 100644 --- a/big_tests/tests/rest_SUITE.erl +++ b/big_tests/tests/rest_SUITE.erl @@ -58,10 +58,7 @@ groups() -> [{admin, [parallel], test_cases()}, {auth, [parallel], auth_test_cases()}, {blank_auth, [parallel], blank_auth_testcases()}, - {roster, [parallel], [list_contacts, - befriend_and_alienate, - befriend_and_alienate_auto, - invalid_roster_operations]} + {roster, [parallel], roster_test_cases()} ]. auth_test_cases() -> @@ -74,17 +71,32 @@ blank_auth_testcases() -> test_cases() -> [non_existent_command_returns404, existent_command_with_missing_arguments_returns404, + invalid_request_body, user_can_be_registered_and_removed, + user_registration_errors, sessions_are_listed, session_can_be_kicked, + session_kick_errors, messages_are_sent_and_received, - messages_error_handling, + message_errors, stanzas_are_sent_and_received, + stanza_errors, messages_are_archived, + message_archive_errors, messages_can_be_paginated, - password_can_be_changed + password_can_be_changed, + password_change_errors ]. +roster_test_cases() -> + [list_contacts, + befriend_and_alienate, + befriend_and_alienate_auto, + list_contacts_errors, + add_contact_errors, + subscription_errors, + delete_contact_errors]. + suite() -> escalus:suite(). @@ -118,7 +130,7 @@ end_per_group(_GroupName, Config) -> escalus:delete_users(Config, escalus:get_users([alice, bob, mike])). init_per_testcase(CaseName, Config) -> - MAMTestCases = [messages_are_archived, messages_can_be_paginated], + MAMTestCases = [messages_are_archived, message_archive_errors, messages_can_be_paginated], rest_helper:maybe_skip_mam_test_cases(CaseName, MAMTestCases, Config). end_per_testcase(CaseName, Config) -> @@ -154,6 +166,9 @@ non_existent_command_returns404(_C) -> existent_command_with_missing_arguments_returns404(_C) -> {?NOT_FOUND, _} = gett(admin, <<"/contacts/">>). +invalid_request_body(_Config) -> + {?BAD_REQUEST, <<"Invalid request body">>} = post(admin, path("users"), <<"kukurydza">>). + user_can_be_registered_and_removed(_Config) -> % list users {?OK, Lusers} = gett(admin, path("users")), @@ -169,14 +184,25 @@ user_can_be_registered_and_removed(_Config) -> % delete user {?NOCONTENT, _} = delete(admin, path("users", ["mike"])), {?OK, Lusers2} = gett(admin, path("users")), - assert_notinlist(<<"mike@", Domain/binary>>, Lusers2), - % invalid jid - CrBadUser = #{username => <<"m@ke">>, password => <<"nicniema">>}, - {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = post(admin, path("users"), CrBadUser), - {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = delete(admin, path("users", ["@mike"])), -%% {?FORBIDDEN, _} = delete(admin, path("users", ["mike"])), % he's already gone, but we -%% can't test it because ejabberd_auth_internal:remove_user/2 always returns ok, grrrr - ok. + assert_notinlist(<<"mike@", Domain/binary>>, Lusers2). + +user_registration_errors(_Config) -> + {AnonUser, AnonDomain} = anon_us(), + {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = + post(admin, path("users"), #{username => <<"m@ke">>, password => <<"nicniema">>}), + {?BAD_REQUEST, <<"Missing password", _/binary>>} = + post(admin, path("users"), #{username => <<"mike">>}), + {?BAD_REQUEST, <<"Missing user name", _/binary>>} = + post(admin, path("users"), #{password => <<"nicniema">>}), + {?FORBIDDEN, <<"Can't register user", _/binary>>} = + post(admin, path("users"), #{username => <<"mike">>, password => <<>>}), + {?FORBIDDEN, <<"Can't register user", _/binary>>} = + post(admin, <<"/users/", AnonDomain/binary>>, #{username => AnonUser, + password => <<"secret">>}), + {?FORBIDDEN, <<"User does not exist or you are not authorised properly">>} = + delete(admin, <<"/users/", AnonDomain/binary, "/", AnonUser/binary>>), + {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = + delete(admin, path("users", ["@mike"])). sessions_are_listed(_) -> % no session @@ -202,6 +228,13 @@ session_can_be_kicked(Config) -> ok end). +session_kick_errors(_Config) -> + {?BAD_REQUEST, <<"Missing user name">>} = + delete(admin, <<"/sessions/", (domain())/binary>>), + %% Resource is matched first, because Cowboy matches path elements from the right + {?BAD_REQUEST, <<"Missing user name">>} = + delete(admin, <<"/sessions/", (domain())/binary, "/resource">>). + messages_are_sent_and_received(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> {M1, M2} = send_messages(Alice, Bob), @@ -211,29 +244,59 @@ messages_are_sent_and_received(Config) -> escalus:assert(is_chat_message, [maps:get(body, M2)], Res1) end). -messages_error_handling(Config) -> - escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), - BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), - {{<<"400">>, _}, <<"Invalid recipient JID">>} = send_message_bin(AliceJID, <<"@noway">>), - {{<<"400">>, _}, <<"Invalid sender JID">>} = send_message_bin(<<"@noway">>, BobJID), - ok - end). +message_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJID = escalus_users:get_jid(Config1, alice), + BobJID = escalus_users:get_jid(Config1, bob), + {?BAD_REQUEST, <<"Missing sender JID">>} = + post(admin, "/messages", #{to => BobJID, body => <<"whatever">>}), + {?BAD_REQUEST, <<"Missing recipient JID">>} = + post(admin, "/messages", #{caller => AliceJID, body => <<"whatever">>}), + {?BAD_REQUEST, <<"Missing message body">>} = + post(admin, "/messages", #{caller => AliceJID, to => BobJID}), + {?BAD_REQUEST, <<"Invalid recipient JID">>} = + send_message_bin(AliceJID, <<"@noway">>), + {?BAD_REQUEST, <<"Invalid sender JID">>} = + send_message_bin(<<"@noway">>, BobJID), + {?BAD_REQUEST, <<"Unknown user">>} = + send_message_bin(<<"baduser@", (domain())/binary>>, BobJID), + {?BAD_REQUEST, <<"Unknown domain">>} = + send_message_bin(<<"baduser@baddomain">>, BobJID). stanzas_are_sent_and_received(Config) -> %% this is to test the API for sending arbitrary stanzas, e.g. message with extra elements escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - send_extended_message(Alice, Bob), + AliceJid = escalus_client:full_jid(Alice), + BobJid = escalus_client:full_jid(Bob), + Stanza = extended_message([{<<"from">>, AliceJid}, {<<"to">>, BobJid}]), + {?NOCONTENT, _} = send_stanza(Stanza), Res = escalus:wait_for_stanza(Bob), ?assertEqual(<<"attribute">>, exml_query:attr(Res, <<"extra">>)), - ?assertEqual(<<"inside the sibling">>, exml_query:path(Res, [{element, <<"sibling">>}, cdata])), - Res1 = send_flawed_stanza(missing_attribute, Alice, Bob), - {?BAD_REQUEST, <<"Missing recipient JID">>} = Res1, - Res2 = send_flawed_stanza(malformed_xml, Alice, Bob), - {?BAD_REQUEST, <<"Malformed stanza">>} = Res2, - ok + ?assertEqual(<<"inside the sibling">>, exml_query:path(Res, [{element, <<"sibling">>}, cdata])) end). +stanza_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + BobJid = escalus_users:get_jid(Config1, bob), + UnknownJid = <<"baduser@", (domain())/binary>>, + {?BAD_REQUEST, <<"Missing recipient JID">>} = + send_stanza(extended_message([{<<"from">>, AliceJid}])), + {?BAD_REQUEST, <<"Missing sender JID">>} = + send_stanza(extended_message([{<<"to">>, BobJid}])), + {?BAD_REQUEST, <<"Invalid recipient JID">>} = + send_stanza(extended_message([{<<"from">>, AliceJid}, {<<"to">>, <<"@invalid">>}])), + {?BAD_REQUEST, <<"Invalid sender JID">>} = + send_stanza(extended_message([{<<"from">>, <<"@invalid">>}, {<<"to">>, BobJid}])), + {?BAD_REQUEST, <<"Unknown domain">>} = + send_stanza(extended_message([{<<"from">>, <<"baduser@baddomain">>}, {<<"to">>, BobJid}])), + {?BAD_REQUEST, <<"Unknown user">>} = + send_stanza(extended_message([{<<"from">>, UnknownJid}, {<<"to">>, BobJid}])), + {?BAD_REQUEST, <<"Malformed stanza">>} = + send_stanza(broken_message([{<<"from">>, AliceJid}, {<<"to">>, BobJid}])), + {?BAD_REQUEST, <<"Missing stanza">>} = + post(admin, <<"/stanzas">>, #{}). + messages_are_archived(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> {M1, _M2} = send_messages(Alice, Bob), @@ -272,6 +335,20 @@ messages_are_archived(Config) -> BobJID = maps:get(sender, Previous2) end). +message_archive_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + User = binary_to_list(escalus_users:get_username(Config1, alice)), + {?NOT_FOUND, <<"Missing owner JID">>} = + gett(admin, "/messages"), + {?BAD_REQUEST, <<"Invalid owner JID">>} = + gett(admin, "/messages/@invalid"), + {?BAD_REQUEST, <<"Invalid interlocutor JID">>} = + gett(admin, "/messages/" ++ User ++ "/@invalid"), + {?BAD_REQUEST, <<"Invalid limit">>} = + gett(admin, "/messages/" ++ User ++ "?limit=x"), + {?BAD_REQUEST, <<"Invalid value of 'before'">>} = + gett(admin, "/messages/" ++ User ++ "?before=x"). + messages_can_be_paginated(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), @@ -323,15 +400,28 @@ password_can_be_changed(Config) -> % now he logs again with the regular one escalus:story(Config, [{bob, 1}], fun(#client{} = _Bob) -> just_dont_do_anything - end), - % test invalid calls - Res1 = putt(admin, path("users", ["bob"]), - #{newpass => <<>>}), - {?BAD_REQUEST, <<"Empty password">>} = Res1, - Res2 = putt(admin, path("users", ["b@b"]), - #{newpass => NewPass}), - {?BAD_REQUEST, <<"Invalid JID">>} = Res2, - ok. + end). + +password_change_errors(Config) -> + Alice = binary_to_list(escalus_users:get_username(Config, alice)), + {AnonUser, AnonDomain} = anon_us(), + Args = #{newpass => <<"secret">>}, + {?FORBIDDEN, <<"Password change not allowed">>} = + putt(admin, <<"/users/", AnonDomain/binary, "/", AnonUser/binary>>, Args), + {?BAD_REQUEST, <<"Missing user name">>} = + putt(admin, path("users", []), Args), + {?BAD_REQUEST, <<"Missing new password">>} = + putt(admin, path("users", [Alice]), #{}), + {?BAD_REQUEST, <<"Empty password">>} = + putt(admin, path("users", [Alice]), #{newpass => <<>>}), + {?BAD_REQUEST, <<"Invalid JID">>} = + putt(admin, path("users", ["@invalid"]), Args). + +anon_us() -> + AnonConfig = [{escalus_users, escalus_ct:get_config(escalus_anon_users)}], + AnonDomain = escalus_users:get_server(AnonConfig, jon), + AnonUser = escalus_users:get_username(AnonConfig, jon), + {AnonUser, AnonDomain}. list_contacts(Config) -> escalus:fresh_story( @@ -365,6 +455,7 @@ befriend_and_alienate(Config) -> check_roster_empty(BobPath), % adds them to rosters {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), + {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), % it is idempotent {?NOCONTENT, _} = post(admin, BobPath, #{jid => AliceJID}), check_roster(BobPath, AliceJID, none, none), check_roster(AlicePath, BobJID, none, none), @@ -455,57 +546,75 @@ befriend_and_alienate_auto(Config) -> ), ok. -invalid_roster_operations(Config) -> - escalus:fresh_story( - Config, [{alice, 1}, {bob, 1}], - fun(Alice, Bob) -> - AliceJID = escalus_utils:jid_to_lower( - escalus_client:short_jid(Alice)), - BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), - AliceS = binary_to_list(AliceJID), - BobS = binary_to_list(BobJID), - AlicePath = lists:flatten(["/contacts/", AliceS]), - % adds them to rosters - {?BAD_REQUEST, <<"Invalid JID">>} = post(admin, AlicePath, #{jid => <<"@invalidjid">>}), - {?BAD_REQUEST, <<"Invalid user JID">>} = post(admin, "/contacts/@invalid_jid", #{jid => BobJID}), - % it is idempotent - {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), - {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), - PutPathA = lists:flatten([AlicePath, "/@invalid_jid"]), - {?BAD_REQUEST, <<"Invalid contact JID">>} = putt(admin, PutPathA, #{action => <<"subscribe">>}), - PutPathB = lists:flatten(["/contacts/@invalid_jid/", BobS]), - {?BAD_REQUEST, <<"Invalid user JID">>} = putt(admin, PutPathB, #{action => <<"subscribe">>}), - PutPathC = lists:flatten([AlicePath, "/", BobS]), - {?BAD_REQUEST, <<"Invalid action">>} = putt(admin, PutPathC, #{action => <<"something stupid">>}), - ManagePath = lists:flatten(["/contacts/", - AliceS, - "/", - BobS, - "/manage" - ]), - {?BAD_REQUEST, <<"Invalid action">>} = putt(admin, ManagePath, #{action => <<"off with his head">>}), - MangePathA = lists:flatten(["/contacts/", - "@invalid", - "/", - BobS, - "/manage" - ]), - {?BAD_REQUEST, <<"Invalid user JID">>} = putt(admin, MangePathA, #{action => <<"connect">>}), - MangePathB = lists:flatten(["/contacts/", - AliceS, - "/", - "@bzzz", - "/manage" - ]), - {?BAD_REQUEST, <<"Invalid contact JID">>} = putt(admin, MangePathB, #{action => <<"connect">>}), - ok - end - ). +list_contacts_errors(_Config) -> + {?NOT_FOUND, <<"Domain not found">>} = gett(admin, contacts_path("baduser@baddomain")). + +add_contact_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + BobJID = escalus_users:get_jid(Config, bob), + AliceS = binary_to_list(escalus_users:get_jid(Config1, alice)), + DomainS = binary_to_list(domain()), + {?BAD_REQUEST, <<"Missing JID">>} = + post(admin, contacts_path(AliceS), #{}), + {?BAD_REQUEST, <<"Invalid JID">>} = + post(admin, contacts_path(AliceS), #{jid => <<"@invalidjid">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = + post(admin, contacts_path("@invalid_jid"), #{jid => BobJID}), + {?NOT_FOUND, <<"The user baduser@", _/binary>>} = + post(admin, contacts_path("baduser@" ++ DomainS), #{jid => BobJID}), + {?NOT_FOUND, <<"Domain not found">>} = + post(admin, contacts_path("baduser@baddomain"), #{jid => BobJID}). + +subscription_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceS = binary_to_list(escalus_users:get_jid(Config1, alice)), + BobS = binary_to_list(escalus_users:get_jid(Config1, bob)), + DomainS = binary_to_list(domain()), + {?BAD_REQUEST, <<"Invalid contact JID">>} = + putt(admin, contacts_path(AliceS, "@invalid_jid"), #{action => <<"subscribe">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = + putt(admin, contacts_path("@invalid_jid", BobS), #{action => <<"subscribe">>}), + {?BAD_REQUEST, <<"Missing action">>} = + putt(admin, contacts_path(AliceS, BobS), #{}), + {?BAD_REQUEST, <<"Missing action">>} = + putt(admin, contacts_manage_path(AliceS, BobS), #{}), + {?BAD_REQUEST, <<"Invalid action">>} = + putt(admin, contacts_path(AliceS, BobS), #{action => <<"something stupid">>}), + {?BAD_REQUEST, <<"Invalid action">>} = + putt(admin, contacts_manage_path(AliceS, BobS), #{action => <<"off with his head">>}), + {?BAD_REQUEST, <<"Invalid user JID">>} = + putt(admin, contacts_manage_path("@invalid", BobS), #{action => <<"connect">>}), + {?BAD_REQUEST, <<"Invalid contact JID">>} = + putt(admin, contacts_manage_path(AliceS, "@bzzz"), #{action => <<"connect">>}), + {?NOT_FOUND, <<"The user baduser@baddomain does not exist">>} = + putt(admin, contacts_manage_path(AliceS, "baduser@baddomain"), #{action => <<"connect">>}), + {?NOT_FOUND, <<"Domain not found">>} = + putt(admin, contacts_manage_path("baduser@baddomain", AliceS), #{action => <<"connect">>}), + {?NOT_FOUND, <<"Cannot remove", _/binary>>} = + putt(admin, contacts_manage_path(AliceS, "baduser@" ++ DomainS), #{action => <<"disconnect">>}). + +delete_contact_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceS = binary_to_list(escalus_users:get_jid(Config1, alice)), + DomainS = binary_to_list(domain()), + {?NOT_FOUND, <<"Cannot remove", _/binary>>} = + delete(admin, contacts_path(AliceS, "baduser@" ++ DomainS)), + {?BAD_REQUEST, <<"Missing contact JID">>} = + delete(admin, contacts_path(AliceS)). %%-------------------------------------------------------------------- %% Helpers %%-------------------------------------------------------------------- +contacts_path(UserJID) -> + "/contacts/" ++ UserJID. + +contacts_path(UserJID, ContactJID) -> + contacts_path(UserJID) ++ "/" ++ ContactJID. + +contacts_manage_path(UserJID, ContactJID) -> + contacts_path(UserJID, ContactJID) ++ "/manage". + send_messages(Alice, Bob) -> AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), @@ -520,39 +629,25 @@ send_message_bin(BFrom, BTo) -> M = #{caller => BFrom, to => BTo, body => <<"whatever">>}, post(admin, <<"/messages">>, M). -send_extended_message(From, To) -> - M = #xmlel{name = <<"message">>, - attrs = [{<<"from">>, escalus_client:full_jid(From)}, - {<<"to">>, escalus_client:full_jid(To)}, - {<<"extra">>, <<"attribute">>}], - children = [#xmlel{name = <<"body">>, - children = [#xmlcdata{content = <<"the body">>}]}, - #xmlel{name = <<"sibling">>, - children = [#xmlcdata{content = <<"inside the sibling">>}]} - ] - }, - M1 = #{stanza => exml:to_binary(M)}, - {?NOCONTENT, _} = post(admin, <<"/stanzas">>, M1), - ok. +send_stanza(StanzaBin) -> + post(admin, <<"/stanzas">>, #{stanza => StanzaBin}). + +broken_message(Attrs) -> + remove_last_character(extended_message(Attrs)). -send_flawed_stanza(missing_attribute, From, _To) -> +remove_last_character(Bin) -> + binary:part(Bin, 0, byte_size(Bin) - 1). + +extended_message(Attrs) -> M = #xmlel{name = <<"message">>, - attrs = [{<<"from">>, escalus_client:full_jid(From)}, - {<<"extra">>, <<"attribute">>}], + attrs = [{<<"extra">>, <<"attribute">>} | Attrs], children = [#xmlel{name = <<"body">>, children = [#xmlcdata{content = <<"the body">>}]}, #xmlel{name = <<"sibling">>, children = [#xmlcdata{content = <<"inside the sibling">>}]} - ] - }, - ct:log("M: ~p", [M]), - M1 = #{stanza => exml:to_binary(M)}, - post(admin, <<"/stanzas">>, M1); -send_flawed_stanza(malformed_xml, _From, _To) -> - % closing > is missing - BadStanza = <<"the body>, - post(admin, <<"/stanzas">>, #{stanza => BadStanza}). - + ] + }, + exml:to_binary(M). check_roster(Path, Jid, Subs, Ask) -> {?OK, R} = gett(admin, Path), diff --git a/src/mongoose_admin_api/mongoose_admin_api_users.erl b/src/mongoose_admin_api/mongoose_admin_api_users.erl index 76b83040d82..eb0f210e91e 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_users.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_users.erl @@ -72,14 +72,15 @@ handle_get(Req, State) -> handle_post(Req, State) -> #{domain := Domain} = cowboy_req:bindings(Req), Args = parse_body(Req), - {UserName, Password} = get_credentials(Args), + UserName = get_user_name(Args), + Password = get_password(Args), case mongoose_account_api:register_user(UserName, Domain, Password) of {exists, Reason} -> throw_error(denied, Reason); {invalid_jid, Reason} -> throw_error(bad_request, Reason); {cannot_register, Reason} -> - throw_error(internal, Reason); + throw_error(denied, Reason); {ok, Result} -> Path = [cowboy_req:uri(Req), "/", UserName], resource_created(Req, State, Path, Result) @@ -116,8 +117,8 @@ handle_delete(Req, State) -> get_user_name(#{username := UserName}) -> UserName; get_user_name(#{}) -> throw_error(bad_request, <<"Missing user name">>). -get_new_password(#{newpass := Password}) -> Password; -get_new_password(#{}) -> throw_error(bad_request, <<"Missing password">>). +get_password(#{password := Password}) -> Password; +get_password(#{}) -> throw_error(bad_request, <<"Missing password">>). -get_credentials(#{username := UserName, password := Password}) -> {UserName, Password}; -get_credentials(#{}) -> throw_error(bad_request, <<"Missing credentials">>). +get_new_password(#{newpass := Password}) -> Password; +get_new_password(#{}) -> throw_error(bad_request, <<"Missing new password">>). From 96592b70c6f1b43014bc2d90753960c53a5f03e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 8 Sep 2022 09:11:35 +0200 Subject: [PATCH 024/117] Cover error cases in inbox_SUITE --- big_tests/tests/inbox_SUITE.erl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/big_tests/tests/inbox_SUITE.erl b/big_tests/tests/inbox_SUITE.erl index 0259eada6b0..8a2d907ce02 100644 --- a/big_tests/tests/inbox_SUITE.erl +++ b/big_tests/tests/inbox_SUITE.erl @@ -123,7 +123,9 @@ groups() -> [ timeout_cleaner_flush_all, rest_api_bin_flush_all, + rest_api_bin_flush_all_errors, rest_api_bin_flush_user, + rest_api_bin_flush_user_errors, xmpp_bin_flush, bin_is_not_included_by_default ]}, @@ -1325,6 +1327,19 @@ rest_api_bin_flush_user(Config) -> check_inbox(Bob, [], #{box => bin}) end). +rest_api_bin_flush_user_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + User = escalus_users:get_username(Config1, alice), + Domain = escalus_users:get_server(Config1, alice), + {{<<"400">>, <<"Bad Request">>}, <<"Invalid number of days">>} = + rest_helper:delete(admin, <<"/inbox/", Domain/binary, "/", User/binary, "/x/bin">>), + {{<<"400">>, <<"Bad Request">>}, <<"Invalid JID">>} = + rest_helper:delete(admin, <<"/inbox/", Domain/binary, "/@/0/bin">>), + {{<<"404">>, <<"Not Found">>}, <<"Domain not found">>} = + rest_helper:delete(admin, <<"/inbox/baddomain/", User/binary, "/0/bin">>), + {{<<"404">>, <<"Not Found">>}, <<"User baduser@", _/binary>>} = + rest_helper:delete(admin, <<"/inbox/", Domain/binary, "/baduser/0/bin">>). + rest_api_bin_flush_all(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) -> create_room_and_make_users_leave(Alice, Bob, Kate), @@ -1337,6 +1352,13 @@ rest_api_bin_flush_all(Config) -> check_inbox(Kate, [], #{box => bin}) end). +rest_api_bin_flush_all_errors(_Config) -> + HostTypePath = uri_string:normalize(#{path => domain_helper:host_type()}), + {{<<"400">>, <<"Bad Request">>}, <<"Invalid number of days">>} = + rest_helper:delete(admin, <<"/inbox/", HostTypePath/binary, "/x/bin">>), + {{<<"404">>, <<"Not Found">>}, <<"Host type not found">>} = + rest_helper:delete(admin, <<"/inbox/bad_host_type/0/bin">>). + timeout_cleaner_flush_all(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) -> create_room_and_make_users_leave(Alice, Bob, Kate), From e644cf2a4573c2fa9e357895bb0747793546c077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 9 Sep 2022 14:09:06 +0200 Subject: [PATCH 025/117] Cover error cases in muc_http_api_SUITE --- big_tests/tests/muc_http_api_SUITE.erl | 130 ++++++++++++++++--------- 1 file changed, 86 insertions(+), 44 deletions(-) diff --git a/big_tests/tests/muc_http_api_SUITE.erl b/big_tests/tests/muc_http_api_SUITE.erl index 57bd37c3d95..2b49c7c51ac 100644 --- a/big_tests/tests/muc_http_api_SUITE.erl +++ b/big_tests/tests/muc_http_api_SUITE.erl @@ -26,7 +26,8 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("exml/include/exml.hrl"). --import(domain_helper, [domain/0]). +-import(domain_helper, [domain/0, secondary_domain/0]). +-import(rest_helper, [post/3, delete/2]). %%-------------------------------------------------------------------- %% Suite configuration @@ -55,8 +56,10 @@ complex() -> ]. failure_response() -> - [failed_invites, - failed_messages]. + [room_creation_errors, + invite_errors, + kick_user_errors, + message_errors]. %%-------------------------------------------------------------------- %% Init & teardown @@ -126,7 +129,7 @@ invite_online_user_to_room(Config) -> recipient => escalus_client:short_jid(Bob), reason => Reason}, {{<<"404">>, _}, <<"Room not found">>} = rest_helper:post(admin, Path, Body), - set_up_room(Config, Alice), + set_up_room(Config, escalus_client:short_jid(Alice)), {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body), Stanza = escalus:wait_for_stanza(Bob), is_direct_invitation(Stanza), @@ -273,60 +276,99 @@ multiparty_multiprotocol(Config) -> user_sees_message_from(Alice, Room, 2)) end). -failed_invites(Config) -> - escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - Name = set_up_room(Config, Alice), - BAlice = escalus_client:short_jid(Alice), - BBob = escalus_client:short_jid(Bob), - % Invite to a non-existent room - {{<<"404">>, _}, <<"Room not found">>} = send_invite(<<"thisroomdoesnotexist">>, BAlice, BBob), - % Invite with a bad jid - {{<<"400">>, _}, <<"Invalid recipient JID">>} = send_invite(Name, BAlice, <<"@badjid">>), - {{<<"400">>, _}, <<"Invalid sender JID">>} = send_invite(Name, <<"@badjid">>, BBob), - ok - end). - -failed_messages(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Name = set_up_room(Config, Alice), - % Message to a non-existent room succeeds in the current implementation - BAlice = escalus_client:short_jid(Alice), - {{<<"204">>, _}, <<>>} = send_message(<<"thisroomdoesnotexist">>, BAlice), - % Message from a bad jid - {{<<"400">>, _}, <<"Invalid sender JID", _/binary>>} = send_message(Name, <<"@badjid">>), - ok - end). +room_creation_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + Name = ?config(room_name, Config), + Body = #{name => Name, owner => AliceJid, nick => <<"nick">>}, + {{<<"400">>, _}, <<"Missing room name">>} = + post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(name, Body)), + {{<<"400">>, _}, <<"Missing nickname">>} = + post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(nick, Body)), + {{<<"400">>, _}, <<"Missing owner JID">>} = + post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(owner, Body)), + {{<<"400">>, _}, <<"Invalid room name">>} = + post(admin, <<"/mucs/", (domain())/binary>>, Body#{name := <<"@invalid">>}), + {{<<"400">>, _}, <<"Invalid owner JID">>} = + post(admin, <<"/mucs/", (domain())/binary>>, Body#{owner := <<"@invalid">>}), + {{<<"404">>, _}, <<"Given user not found">>} = + post(admin, <<"/mucs/", (domain())/binary>>, Body#{owner := <<"baduser@baddomain">>}). + +invite_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + BobJid = escalus_users:get_jid(Config1, bob), + Name = set_up_room(Config1, AliceJid), + Path = path([Name, "participants"]), + Body = #{sender => AliceJid, recipient => BobJid, reason => <<"Join this room!">>}, + {{<<"400">>, _}, <<"Missing sender JID">>} = + post(admin, Path, maps:remove(sender, Body)), + {{<<"400">>, _}, <<"Missing recipient JID">>} = + post(admin, Path, maps:remove(recipient, Body)), + {{<<"400">>, _}, <<"Missing invite reason">>} = + post(admin, Path, maps:remove(reason, Body)), + {{<<"400">>, _}, <<"Invalid recipient JID">>} = + post(admin, Path, Body#{recipient := <<"@badjid">>}), + {{<<"400">>, _}, <<"Invalid sender JID">>} = + post(admin, Path, Body#{sender := <<"@badjid">>}), + {{<<"404">>, _}, <<"MUC domain not found">>} = + post(admin, <<"/mucs/baddomain/", Name/binary, "/participants">>, Body), + {{<<"404">>, _}, <<"Room not found">>} = + post(admin, path(["thisroomdoesnotexist", "participants"]), Body). + +kick_user_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + Name = ?config(room_name, Config1), + {{<<"404">>, _}, <<"Room not found">>} = delete(admin, path([Name, "nick"])), + set_up_room(Config1, AliceJid), + mongoose_helper:wait_until(fun() -> check_if_moderator_not_found(Name) end, ok), + %% Alice sends presence to the room, making her the moderator + {ok, Alice} = escalus_client:start(Config1, alice, <<"res1">>), + escalus:send(Alice, muc_helper:stanza_muc_enter_room(Name, <<"ali">>)), + %% Alice gets her affiliation information and the room's subject line. + escalus:wait_for_stanzas(Alice, 2), + %% Kicking a non-existent nick succeeds in the current implementation + {{<<"204">>, _}, <<>>} = delete(admin, path([Name, "nick"])), + escalus_client:stop(Config, Alice). + +%% @doc Check if the sequence below has already happened: +%% 1. Room notification to the owner is bounced back, because the owner is offline +%% 2. The owner is removed from the online users +%% As a result, a request to kick a user returns Error 404 +check_if_moderator_not_found(RoomName) -> + case delete(admin, path([RoomName, "nick"])) of + {{<<"404">>, _}, <<"Moderator user not found">>} -> ok; + {{<<"204">>, _}, _} -> not_yet + end. + +message_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + Name = set_up_room(Config1, AliceJid), + Path = path([Name, "messages"]), + Body = #{from => AliceJid, body => <<"Greetings!">>}, + % Message to a non-existent room succeeds in the current implementation + {{<<"204">>, _}, <<>>} = post(admin, path(["thisroomdoesnotexist", "messages"]), Body), + {{<<"400">>, _}, <<"Missing message body">>} = post(admin, Path, maps:remove(body, Body)), + {{<<"400">>, _}, <<"Missing sender JID">>} = post(admin, Path, maps:remove(from, Body)), + {{<<"400">>, _}, <<"Invalid sender JID">>} = post(admin, Path, Body#{from := <<"@invalid">>}). %%-------------------------------------------------------------------- %% Ancillary (adapted from the MUC suite) %%-------------------------------------------------------------------- -set_up_room(Config, Alice) -> +set_up_room(Config, OwnerJID) -> % create a room first Name = ?config(room_name, Config), Path = path([]), Body = #{name => Name, - owner => escalus_client:short_jid(Alice), + owner => OwnerJID, nick => <<"ali">>}, Res = rest_helper:post(admin, Path, Body), {{<<"201">>, _}, Name} = Res, Name. -send_invite(RoomName, BinFrom, BinTo) -> - Path = path([RoomName, "participants"]), - Reason = <<"I think you'll like this room!">>, - Body = #{sender => BinFrom, - recipient => BinTo, - reason => Reason}, - rest_helper:post(admin, Path, Body). - -send_message(RoomName, BinFrom) -> - Path = path([RoomName, "messages"]), - Message = <<"Greetings!">>, - Body = #{from => BinFrom, - body => Message}, - rest_helper:post(admin, Path, Body). - make_distinct_name(Prefix) -> {_, S, US} = os:timestamp(), L = lists:flatten([integer_to_list(S rem 100), ".", integer_to_list(US)]), From 4583d1318e653a167b4fed36c60f2cf0d87f23c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Mon, 12 Sep 2022 13:50:28 +0200 Subject: [PATCH 026/117] Cover error cases in muc_light_http_api_SUITE --- big_tests/tests/muc_light_http_api_SUITE.erl | 144 +++++++++++++------ 1 file changed, 101 insertions(+), 43 deletions(-) diff --git a/big_tests/tests/muc_light_http_api_SUITE.erl b/big_tests/tests/muc_light_http_api_SUITE.erl index 9c3bc1c9fa4..653af74e85e 100644 --- a/big_tests/tests/muc_light_http_api_SUITE.erl +++ b/big_tests/tests/muc_light_http_api_SUITE.erl @@ -30,6 +30,7 @@ -import(distributed_helper, [subhost_pattern/1]). -import(domain_helper, [host_type/0, domain/0]). -import(config_parser_helper, [mod_config/2]). +-import(rest_helper, [putt/3, post/3, delete/2]). %%-------------------------------------------------------------------- %% Suite configuration @@ -52,10 +53,11 @@ success_response() -> ]. negative_response() -> - [delete_non_existent_room, - create_non_unique_room, - create_room_on_non_existing_muc_server - ]. + [create_room_errors, + create_identifiable_room_errors, + invite_to_room_errors, + send_message_errors, + delete_room_errors]. %%-------------------------------------------------------------------- %% Init & teardown @@ -193,44 +195,101 @@ delete_room(Config) -> Alice, [Bob, Kate]) end). -delete_non_existent_room(Config) -> - RoomID = atom_to_binary(?FUNCTION_NAME), - RoomName = <<"wonderland">>, - escalus:fresh_story(Config, - [{alice, 1}, {bob, 1}, {kate, 1}], - fun(Alice, Bob, Kate)-> - {{<<"404">>, _}, <<"Cannot remove not existing room">>} = - check_delete_room(Config, RoomName, RoomID, - <<"some_non_existent_room">>, - Alice, [Bob, Kate]) - end). - -create_non_unique_room(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Path = path([muc_light_domain()]), - RandBits = base16:encode(crypto:strong_rand_bytes(5)), - Name = <<"wonderland">>, - RoomID = <<"just_some_id_", RandBits/binary>>, - Body = #{ id => RoomID, - name => Name, - owner => escalus_client:short_jid(Alice), - subject => <<"Lewis Carol">> - }, - {{<<"201">>, _}, _RoomJID} = rest_helper:putt(admin, Path, Body), - {{<<"403">>, _}, <<"Room already exists">>} = rest_helper:putt(admin, Path, Body), - ok - end). - -create_room_on_non_existing_muc_server(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Path = path([domain_helper:domain()]), - Name = <<"wonderland">>, - Body = #{ name => Name, - owner => escalus_client:short_jid(Alice), - subject => <<"Lewis Carol">> - }, - {{<<"404">>,<<"Not Found">>}, _} = rest_helper:post(admin, Path, Body) - end). +create_room_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + Path = path([muc_light_domain()]), + Body = #{name => <<"Name">>, owner => AliceJid, subject => <<"Lewis Carol">>}, + {{<<"400">>, _}, <<"Missing room name">>} = + post(admin, Path, maps:remove(name, Body)), + {{<<"400">>, _}, <<"Missing owner JID">>} = + post(admin, Path, maps:remove(owner, Body)), + {{<<"400">>, _}, <<"Missing room subject">>} = + post(admin, Path, maps:remove(subject, Body)), + {{<<"400">>, _}, <<"Invalid owner JID">>} = + post(admin, Path, Body#{owner := <<"@invalid">>}), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + post(admin, path([domain_helper:domain()]), Body). + +create_identifiable_room_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + Path = path([muc_light_domain()]), + Body = #{id => <<"ID">>, name => <<"NameA">>, owner => AliceJid, subject => <<"Lewis Carol">>}, + {{<<"201">>, _}, _RoomJID} = putt(admin, Path, Body#{id => <<"ID1">>}), + % Fails to create a room with the same ID + {{<<"400">>, _}, <<"Missing room ID">>} = + putt(admin, Path, maps:remove(id, Body)), + {{<<"400">>, _}, <<"Missing room name">>} = + putt(admin, Path, maps:remove(name, Body)), + {{<<"400">>, _}, <<"Missing owner JID">>} = + putt(admin, Path, maps:remove(owner, Body)), + {{<<"400">>, _}, <<"Missing room subject">>} = + putt(admin, Path, maps:remove(subject, Body)), + {{<<"400">>, _}, <<"Invalid owner JID">>} = + putt(admin, Path, Body#{owner := <<"@invalid">>}), + {{<<"403">>, _}, <<"Room already exists">>} = + putt(admin, Path, Body#{id := <<"ID1">>, name := <<"NameB">>}), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + putt(admin, path([domain_helper:domain()]), Body). + +invite_to_room_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + BobJid = escalus_users:get_jid(Config1, bob), + Name = jid:nodeprep(<<(escalus_users:get_username(Config1, alice))/binary, "-room">>), + muc_light_helper:create_room(Name, muc_light_domain(), alice, [], Config1, <<"v1">>), + Path = path([muc_light_domain(), Name, "participants"]), + Body = #{sender => AliceJid, recipient => BobJid}, + {{<<"400">>, _}, <<"Missing recipient JID">>} = + rest_helper:post(admin, Path, maps:remove(recipient, Body)), + {{<<"400">>, _}, <<"Missing sender JID">>} = + rest_helper:post(admin, Path, maps:remove(sender, Body)), + {{<<"400">>, _}, <<"Invalid recipient JID">>} = + rest_helper:post(admin, Path, Body#{recipient := <<"@invalid">>}), + {{<<"400">>, _}, <<"Invalid sender JID">>} = + rest_helper:post(admin, Path, Body#{sender := <<"@invalid">>}), + {{<<"403">>, _}, <<"Given user does not occupy this room">>} = + rest_helper:post(admin, Path, Body#{sender := BobJid, recipient := AliceJid}), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + rest_helper:post(admin, path([domain(), Name, "participants"]), Body). + +send_message_errors(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + BobJid = escalus_users:get_jid(Config1, bob), + Name = jid:nodeprep(<<(escalus_users:get_username(Config1, alice))/binary, "-room">>), + muc_light_helper:create_room(Name, muc_light_domain(), alice, [], Config1, <<"v1">>), + Path = path([muc_light_domain(), Name, "messages"]), + Body = #{from => AliceJid, body => <<"hello">>}, + {{<<"204">>, _}, <<>>} = + rest_helper:post(admin, Path, Body), + {{<<"400">>, _}, <<"Missing message body">>} = + rest_helper:post(admin, Path, maps:remove(body, Body)), + {{<<"400">>, _}, <<"Missing sender JID">>} = + rest_helper:post(admin, Path, maps:remove(from, Body)), + {{<<"400">>, _}, <<"Invalid sender JID">>} = + rest_helper:post(admin, Path, Body#{from := <<"@invalid">>}), + {{<<"403">>, _}, <<"Given user does not occupy this room">>} = + rest_helper:post(admin, Path, Body#{from := BobJid}), + {{<<"403">>, _}, <<"Given user does not occupy this room">>} = + rest_helper:post(admin, path([muc_light_domain(), "badroom", "messages"]), Body), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + rest_helper:post(admin, path([domain(), Name, "messages"]), Body). + +delete_room_errors(_Config) -> + {{<<"400">>, _}, <<"Invalid room ID or domain name">>} = + delete(admin, path([muc_light_domain(), "@badroom", "management"])), + {{<<"404">>, _}, _} = + delete(admin, path([muc_light_domain()])), + {{<<"404">>, _}, _} = + delete(admin, path([muc_light_domain(), "badroom"])), + {{<<"404">>, _}, <<"Cannot remove not existing room">>} = + delete(admin, path([muc_light_domain(), "badroom", "management"])), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + delete(admin, path([domain(), "badroom", "management"])), + {{<<"404">>, _}, <<"MUC Light server not found">>} = + delete(admin, path(["baddomain", "badroom", "management"])). %%-------------------------------------------------------------------- %% Ancillary (borrowed and adapted from the MUC and MUC Light suites) @@ -286,7 +345,6 @@ check_delete_room(_Config, RoomName, RoomIDToCreate, RoomIDToDelete, RoomOwner, Path = path([muc_light_domain(), RoomIDToDelete, "management"]), rest_helper:delete(admin, Path). - %%-------------------------------------------------------------------- %% Helpers %%-------------------------------------------------------------------- From ecd29ce3a2aebade6f98a189f16c284923b4280f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Wed, 14 Sep 2022 11:27:20 +0200 Subject: [PATCH 027/117] Removed disco_local_features/3 from ignore_xref --- src/ejabberd_local.erl | 2 +- src/inbox/mod_inbox.erl | 4 ++-- src/mam/mod_mam_pm.erl | 5 ++--- src/mod_adhoc.erl | 2 +- src/mod_amp.erl | 2 +- src/mod_auth_token.erl | 2 +- src/mod_blocking.erl | 2 +- src/mod_caps.erl | 2 +- src/mod_carboncopy.erl | 2 +- src/mod_disco.erl | 4 ++-- src/privacy/mod_privacy.erl | 2 +- src/pubsub/mod_pubsub.erl | 2 +- 12 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 7817b45f677..565ea0dd6af 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -62,7 +62,7 @@ -export([do_route/4]). --ignore_xref([disco_local_features/3, do_route/4, get_iq_callback/1, +-ignore_xref([do_route/4, get_iq_callback/1, process_iq_reply/4, start_link/0]). -include("mongoose.hrl"). diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index 0d7e9d6f7cd..f73cadc56fa 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -31,8 +31,8 @@ ]). -ignore_xref([ - disco_local_features/3, filter_local_packet/1, get_personal_data/3, - inbox_unread_count/2, remove_domain/3, remove_user/3, user_send_packet/4 + filter_local_packet/1, get_personal_data/3, inbox_unread_count/2, + remove_domain/3, remove_user/3, user_send_packet/4 ]). -export([process_inbox_boxes/1]). diff --git a/src/mam/mod_mam_pm.erl b/src/mam/mod_mam_pm.erl index ebf5fc938e4..9b2a0eddf24 100644 --- a/src/mam/mod_mam_pm.erl +++ b/src/mam/mod_mam_pm.erl @@ -64,9 +64,8 @@ -ignore_xref([archive_message_from_ct/1, archive_size/2, archive_size_with_host_type/3, delete_archive/2, - determine_amp_strategy/5, disco_local_features/3, filter_packet/1, - get_personal_data/3, remove_user/3, sm_filter_offline_message/4, - user_send_packet/4]). + determine_amp_strategy/5, filter_packet/1, get_personal_data/3, + remove_user/3, sm_filter_offline_message/4, user_send_packet/4]). -type host_type() :: mongooseim:host_type(). diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index b094d3b3d9b..a7b6f56c7e9 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -48,7 +48,7 @@ disco_sm_features/1, ping_command/4]). --ignore_xref([disco_local_features/3, disco_local_identity/1, disco_local_items/1, +-ignore_xref([disco_local_identity/1, disco_local_items/1, disco_sm_features/1, disco_sm_identity/1, disco_sm_items/1, ping_command/4, process_local_iq/5, process_sm_iq/5]). diff --git a/src/mod_amp.erl b/src/mod_amp.erl index ac0e65b3324..7b92d4d9b43 100644 --- a/src/mod_amp.erl +++ b/src/mod_amp.erl @@ -15,7 +15,7 @@ c2s_stream_features/3 ]). --ignore_xref([c2s_stream_features/3, disco_local_features/3, run_initial_check/2]). +-ignore_xref([c2s_stream_features/3, run_initial_check/2]). -include("amp.hrl"). -include("mongoose.hrl"). diff --git a/src/mod_auth_token.erl b/src/mod_auth_token.erl index 18c614d23a0..22114b725ba 100644 --- a/src/mod_auth_token.erl +++ b/src/mod_auth_token.erl @@ -50,7 +50,7 @@ -ignore_xref([ behaviour_info/1, clean_tokens/3, datetime_to_seconds/1, deserialize/1, - disco_local_features/3, expiry_datetime/3, get_key_for_host_type/2, process_iq/5, + expiry_datetime/3, get_key_for_host_type/2, process_iq/5, revoke/2, revoke_token_command/1, seconds_to_datetime/1, serialize/1, token/3, token_with_mac/2 ]). diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 23571afb4d9..a1f99221583 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -17,7 +17,7 @@ disco_local_features/3 ]). --ignore_xref([disco_local_features/3, process_iq_get/5, process_iq_set/4]). +-ignore_xref([process_iq_get/5, process_iq_set/4]). -include("jlib.hrl"). -include("mod_privacy.hrl"). diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 8a0c5a63495..7118f6c3363 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -53,7 +53,7 @@ -export([delete_caps/1, make_disco_hash/2]). -ignore_xref([c2s_broadcast_recipients/5, c2s_filter_packet/5, c2s_presence_in/4, - caps_stream_features/3, delete_caps/1, disco_info/1, disco_local_features/3, + caps_stream_features/3, delete_caps/1, disco_info/1, disco_local_identity/1, make_disco_hash/2, read_caps/1, start_link/2, user_receive_packet/5, user_send_packet/4]). diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 2d747c3cb8c..8e228f174fd 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -49,7 +49,7 @@ %% Tests -export([should_forward/3]). --ignore_xref([disco_local_features/3, is_carbon_copy/1, remove_connection/5, +-ignore_xref([is_carbon_copy/1, remove_connection/5, should_forward/3, user_receive_packet/5, user_send_packet/4]). -define(CC_KEY, 'cc'). diff --git a/src/mod_disco.erl b/src/mod_disco.erl index fc96d018c5f..eadefea3fd9 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -50,8 +50,8 @@ disco_local_features/3, disco_info/1]). --ignore_xref([disco_info/1, disco_local_features/3, disco_local_identity/1, - disco_local_items/1, disco_sm_identity/1, disco_sm_items/1]). +-ignore_xref([disco_info/1, disco_local_identity/1, disco_local_items/1, + disco_sm_identity/1, disco_sm_items/1]). -include("mongoose.hrl"). -include("jlib.hrl"). diff --git a/src/privacy/mod_privacy.erl b/src/privacy/mod_privacy.erl index 59116227827..c1691ba373c 100644 --- a/src/privacy/mod_privacy.erl +++ b/src/privacy/mod_privacy.erl @@ -52,7 +52,7 @@ -ignore_xref([ behaviour_info/1, check_packet/5, get_user_list/3, process_iq_get/5, process_iq_set/4, remove_user/3, updated_list/3, - remove_user/3, remove_domain/3, disco_local_features/3]). + remove_user/3, remove_domain/3]). -include("jlib.hrl"). -include("mod_privacy.hrl"). diff --git a/src/pubsub/mod_pubsub.erl b/src/pubsub/mod_pubsub.erl index 10bc029b9d2..c7a9a552686 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -120,7 +120,7 @@ {?MOD_PUBSUB_DB_BACKEND, set_subscription_opts, 4}, {?MOD_PUBSUB_DB_BACKEND, stop, 0}, affiliation_to_string/1, caps_recognised/4, create_node/7, default_host/0, - delete_item/4, delete_node/3, disco_local_features/3, disco_sm_features/1, + delete_item/4, delete_node/3, disco_sm_features/1, disco_sm_identity/1, disco_sm_items/1, extended_error/3, get_cached_item/2, get_item/3, get_items/2, get_personal_data/3, handle_pep_authorization_response/1, handle_remote_hook/4, host/2, in_subscription/5, iq_sm/4, node_action/4, node_call/4, From a08a82e5896813f036b4b3da3290b0e651bca694 Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Fri, 9 Sep 2022 16:53:25 +0200 Subject: [PATCH 028/117] Fix types in subscribeAllToAll roster API --- big_tests/tests/graphql_helper.erl | 5 +- big_tests/tests/graphql_roster_SUITE.erl | 76 +++++++++++++++++-- priv/graphql/schemas/admin/roster.gql | 2 +- ...mongoose_graphql_roster_admin_mutation.erl | 3 +- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index 99b651ec733..ea260715fc0 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -106,7 +106,7 @@ init_domain_admin_handler(Config) -> Password = base16:encode(crypto:strong_rand_bytes(8)), Creds = {<<"admin@", Domain/binary>>, Password}, ok = domain_helper:set_domain_password(mim(), Domain, Password), - add_specs([{protocol, http}, {domain_admin, Creds}, {schema_endpoint, domain_admin} + add_specs([{protocol, http}, {domain_admin, Creds}, {schema_endpoint, domain_admin} | Config]); false -> {skip, require_rdbms} end. @@ -143,6 +143,9 @@ get_unauthorized({Code, #{<<"errors">> := Errors}}) -> assert_response_code(unauthorized, Code), ?assertEqual(<<"no_permissions">>, ErrorCode). +get_bad_request({Code, _Msg}) -> + assert_response_code(bad_request, Code). + get_coercion_err_msg({Code, #{<<"errors">> := [Error]}}) -> assert_response_code(bad_request, Code), ?assertEqual(<<"input_coercion">>, get_value([extensions, code], Error)), diff --git a/big_tests/tests/graphql_roster_SUITE.erl b/big_tests/tests/graphql_roster_SUITE.erl index b61e29f519b..5cf2da20424 100644 --- a/big_tests/tests/graphql_roster_SUITE.erl +++ b/big_tests/tests/graphql_roster_SUITE.erl @@ -5,7 +5,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_listener_port/1, get_listener_config/1, get_ok_value/2, get_err_value/2, get_err_msg/1, - get_err_msg/2, user_to_jid/1, user_to_bin/1, get_unauthorized/1]). + get_err_msg/2, get_bad_request/1, user_to_jid/1, user_to_bin/1, + get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("../../include/mod_roster.hrl"). @@ -55,8 +56,12 @@ admin_roster_tests() -> admin_set_mutual_subscription_try_disconnect_nonexistent_users, admin_subscribe_to_all, admin_subscribe_to_all_with_wrong_user, + admin_subscribe_to_all_no_groups, + admin_subscribe_to_all_without_arguments, admin_subscribe_all_to_all, admin_subscribe_all_to_all_with_wrong_user, + admin_subscribe_all_to_all_no_groups, + admin_subscribe_all_to_all_without_arguments, admin_list_contacts, admin_list_contacts_wrong_user, admin_get_contact, @@ -81,9 +86,13 @@ domain_admin_tests() -> domain_admin_subscribe_to_all_no_permission, admin_subscribe_to_all, domain_admin_subscribe_to_all_with_wrong_user, + admin_subscribe_to_all_no_groups, + admin_subscribe_to_all_without_arguments, domain_admin_subscribe_all_to_all_no_permission, admin_subscribe_all_to_all, domain_admin_subscribe_all_to_all_with_wrong_user, + admin_subscribe_all_to_all_no_groups, + admin_subscribe_all_to_all_without_arguments, admin_list_contacts, domain_admin_list_contacts_wrong_user, domain_admin_list_contacts_no_permission, @@ -314,6 +323,23 @@ admin_subscribe_to_all_with_wrong_user_story(Config, Alice, Bob) -> check_contacts([Bob], Alice), check_contacts([Alice], Bob). +admin_subscribe_to_all_no_groups(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}, {kate, 1}], + fun admin_subscribe_to_all_no_groups_story/4). + +admin_subscribe_to_all_no_groups_story(Config, Alice, Bob, Kate) -> + EmptyGroups = [], + Res = admin_subscribe_to_all(Alice, [Bob, Kate], null, Config), + check_if_created_succ(?SUBSCRIBE_TO_ALL_PATH, Res), + + check_contacts([Bob, Kate], Alice, EmptyGroups), + check_contacts([Alice], Bob, EmptyGroups), + check_contacts([Alice], Kate, EmptyGroups). + +admin_subscribe_to_all_without_arguments(Config) -> + Res = admin_subscribe_to_all_no_args(Config), + get_bad_request(Res). + admin_subscribe_all_to_all(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun admin_subscribe_all_to_all_story/4). @@ -340,6 +366,23 @@ admin_subscribe_all_to_all_with_wrong_user_story(Config, Alice, Bob) -> check_contacts([Bob], Alice), check_contacts([Alice], Bob). +admin_subscribe_all_to_all_no_groups(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}, {kate, 1}], + fun admin_subscribe_all_to_all_no_groups_story/4). + +admin_subscribe_all_to_all_no_groups_story(Config, Alice, Bob, Kate) -> + EmptyGroups = [], + Res = admin_subscribe_all_to_all([Alice, Bob, Kate], null, Config), + check_if_created_succ(?SUBSCRIBE_ALL_TO_ALL_PATH, Res), + + check_contacts([Bob, Kate], Alice, EmptyGroups), + check_contacts([Alice, Kate], Bob, EmptyGroups), + check_contacts([Alice, Bob], Kate, EmptyGroups). + +admin_subscribe_all_to_all_without_arguments(Config) -> + Res = admin_subscribe_all_to_all_no_args(Config), + get_bad_request(Res). + admin_list_contacts(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_list_contacts_story/3). @@ -655,6 +698,9 @@ user_add_contact(User, Contact, Config) -> user_add_contact(User, Contact, Name, ?DEFAULT_GROUPS, Config). check_contacts(ContactClients, User) -> + check_contacts(ContactClients, User, ?DEFAULT_GROUPS). + +check_contacts(ContactClients, User, ContactGroups) -> Expected = [escalus_utils:jid_to_lower(escalus_client:short_jid(Client)) || Client <- ContactClients], ExpectedNames = [escalus_client:username(Client) || Client <- ContactClients], @@ -663,7 +709,7 @@ check_contacts(ContactClients, User) -> ActualNames = [ Name || #roster{name = Name} <- ActualContacts], ?assertEqual(lists:sort(Expected), lists:sort(Actual)), ?assertEqual(lists:sort(ExpectedNames), lists:sort(ActualNames)), - [?assertEqual(?DEFAULT_GROUPS, Groups) || #roster{groups = Groups} <- ActualContacts]. + [?assertEqual(ContactGroups, Groups) || #roster{groups = Groups} <- ActualContacts]. check_if_created_succ(Path, Res) -> OkList = get_ok_value(Path, Res), @@ -692,10 +738,16 @@ get_roster(User, Contact) -> full]). make_contacts(Users) -> - [make_contact(U) || U <- Users]. + make_contacts(Users, ?DEFAULT_GROUPS). + +make_contacts(Users, Groups) -> + [make_contact(U, Groups) || U <- Users]. make_contact(U) -> - #{jid => user_to_bin(U), name => escalus_utils:get_username(U), groups => ?DEFAULT_GROUPS}. + make_contact(U, ?DEFAULT_GROUPS). + +make_contact(U, Groups) -> + #{jid => user_to_bin(U), name => escalus_utils:get_username(U), groups => Groups}. %% Commands @@ -725,13 +777,25 @@ admin_mutual_subscription(User, Contact, Action, Config) -> execute_command(<<"roster">>, <<"setMutualSubscription">>, Vars, Config). admin_subscribe_to_all(User, Contacts, Config) -> - Vars = #{user => make_contact(User), contacts => make_contacts(Contacts)}, + admin_subscribe_to_all(User, Contacts, ?DEFAULT_GROUPS, Config). + +admin_subscribe_to_all(User, Contacts, Groups, Config) -> + Vars = #{user => make_contact(User, Groups), contacts => make_contacts(Contacts, Groups)}, execute_command(<<"roster">>, <<"subscribeToAll">>, Vars, Config). +admin_subscribe_to_all_no_args(Config) -> + execute_command(<<"roster">>, <<"subscribeToAll">>, #{}, Config). + admin_subscribe_all_to_all(Users, Config) -> - Vars = #{contacts => make_contacts(Users)}, + admin_subscribe_all_to_all(Users, ?DEFAULT_GROUPS, Config). + +admin_subscribe_all_to_all(Users, Groups, Config) -> + Vars = #{contacts => make_contacts(Users, Groups)}, execute_command(<<"roster">>, <<"subscribeAllToAll">>, Vars, Config). +admin_subscribe_all_to_all_no_args(Config) -> + execute_command(<<"roster">>, <<"subscribeAllToAll">>, #{}, Config). + admin_list_contacts(User, Config) -> Vars = #{user => user_to_bin(User)}, execute_command(<<"roster">>, <<"listContacts">>, Vars, Config). diff --git a/priv/graphql/schemas/admin/roster.gql b/priv/graphql/schemas/admin/roster.gql index a170fb5e767..e9927746845 100644 --- a/priv/graphql/schemas/admin/roster.gql +++ b/priv/graphql/schemas/admin/roster.gql @@ -24,7 +24,7 @@ type RosterAdminMutation @protected{ subscribeToAll(user: ContactInput!, contacts: [ContactInput!]!): [String]! @protected(type: DOMAIN, args: ["user.jid"]) "Set mutual subscriptions between all of the given contacts" - subscribeAllToAll(contacts: [ContactInput!]): [String]! + subscribeAllToAll(contacts: [ContactInput!]!): [String]! @protected(type: DOMAIN, args: ["contacts.jid"]) } diff --git a/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl index 3ec26854c13..c2728ab0070 100644 --- a/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl @@ -87,4 +87,5 @@ do_subscribe_all_to_all([User | Contacts]) -> do_subscribe_to_all(User, Contacts) ++ do_subscribe_all_to_all(Contacts). contact_input_map_to_tuple(#{<<"jid">> := JID, <<"name">> := Name, <<"groups">> := Groups}) -> - {JID, Name, Groups}. + Groups1 = null_to_default(Groups, []), + {JID, Name, Groups1}. From 3de653156d086b4c1c5525aef4d4e3758d190dde Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 31 Aug 2022 12:02:36 +0200 Subject: [PATCH 029/117] Adding server schema --- big_tests/default.spec | 1 + big_tests/dynamic_domains.spec | 1 + big_tests/tests/graphql_server_SUITE.erl | 149 ++++++++++++++++++ priv/graphql/schemas/admin/admin_schema.gql | 4 + priv/graphql/schemas/admin/server.gql | 31 ++++ src/admin_extra/service_admin_extra_node.erl | 8 +- src/ejabberd_admin.erl | 59 +------ .../admin/mongoose_graphql_admin_mutation.erl | 2 + .../admin/mongoose_graphql_admin_query.erl | 20 +-- ...mongoose_graphql_server_admin_mutation.erl | 21 +++ .../mongoose_graphql_server_admin_query.erl | 20 +++ src/graphql/mongoose_graphql.erl | 2 + src/graphql/mongoose_graphql_enum.erl | 3 +- src/graphql/mongoose_graphql_errors.erl | 3 + src/server_api.erl | 57 +++++++ test/mongoose_config_SUITE.erl | 2 +- 16 files changed, 310 insertions(+), 73 deletions(-) create mode 100644 big_tests/tests/graphql_server_SUITE.erl create mode 100644 priv/graphql/schemas/admin/server.gql create mode 100644 src/graphql/admin/mongoose_graphql_server_admin_mutation.erl create mode 100644 src/graphql/admin/mongoose_graphql_server_admin_query.erl create mode 100644 src/server_api.erl diff --git a/big_tests/default.spec b/big_tests/default.spec index 67881165cfb..17ed70b62db 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -43,6 +43,7 @@ {suites, "tests", graphql_mnesia_SUITE}. {suites, "tests", graphql_vcard_SUITE}. {suites, "tests", graphql_http_upload_SUITE}. +{suites, "tests", graphql_server_SUITE}. {suites, "tests", graphql_metric_SUITE}. {suites, "tests", inbox_SUITE}. {suites, "tests", inbox_extensions_SUITE}. diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index a8ebfd80f4e..9346d9d0cd7 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -59,6 +59,7 @@ {suites, "tests", graphql_token_SUITE}. {suites, "tests", graphql_mnesia_SUITE}. {suites, "tests", graphql_http_upload_SUITE}. +{suites, "tests", graphql_server_SUITE}. {suites, "tests", graphql_metric_SUITE}. {suites, "tests", inbox_SUITE}. diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl new file mode 100644 index 00000000000..989b96c4f90 --- /dev/null +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -0,0 +1,149 @@ +-module(graphql_server_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [is_sm_distributed/0, + mim/0, mim2/0, mim3/0, + require_rpc_nodes/1]). +-import(domain_helper, [host_type/0, domain/0]). +-import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, + get_err_msg/1, get_err_code/1]). + +-include_lib("eunit/include/eunit.hrl"). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [{group, admin_http}, + {group, admin_cli}]. + +groups() -> + [{admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {server_tests, [], admin_tests()}, + {clustering_two_tests, [], clustering_tests()}]. + +admin_groups() -> + [{group, server_tests}, + {group, clustering_two_tests}]. + +admin_tests() -> + [get_cookie_test, + get_loglevel_test, + get_status_test, + set_loglevel_test]. + +clustering_tests() -> + [get_status_test, join_successful_prompt]. + +init_per_suite(Config) -> + Config1 = dynamic_modules:save_modules(host_type(), Config), + #{node := Node1} = RPCNode1 = mim(), + #{node := Node2} = RPCNode2 = mim2(), + #{node := Node3} = RPCNode3 = mim3(), + Config2 = ejabberd_node_utils:init(RPCNode1, Config1), + Config3 = ejabberd_node_utils:init(RPCNode2, Config2), + Config4 = ejabberd_node_utils:init(RPCNode3, Config3), + NodeCtlPath = distributed_helper:ctl_path(Node1, Config4), + Node2CtlPath = distributed_helper:ctl_path(Node2, Config4), + Node3CtlPath = distributed_helper:ctl_path(Node3, Config4), + escalus:init_per_suite([{ctl_path_atom(Node1), NodeCtlPath}, + {ctl_path_atom(Node2), Node2CtlPath}, + {ctl_path_atom(Node3), Node3CtlPath}] + ++ Config3). + +ctl_path_atom(NodeName) -> + CtlString = atom_to_list(NodeName) ++ "_ctl", + list_to_atom(CtlString). + +end_per_suite(Config) -> + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +init_per_group(admin_http, Config) -> + graphql_helper:init_admin_handler(Config); +init_per_group(admin_cli, Config) -> + graphql_helper:init_admin_cli(Config); +init_per_group(clustering_two_tests, Config) -> + case is_sm_distributed() of + true -> + Config; + {false, Backend} -> + ct:pal("Backend ~p doesn't support distributed tests", [Backend]), + {skip, nondistributed_sm} + end; +init_per_group(_, Config) -> + Config. + +end_per_group(Group, _Config) when Group =:= admin_http; + Group =:= admin_cli -> + graphql_helper:clean(); +end_per_group(_, _Config) -> + escalus_fresh:clean(). + +get_cookie_test(Config) -> + Result = get_ok_value([data, server, getCookie], get_cookie(Config)), + ?assert(is_binary(Result)). + +get_loglevel_test(Config) -> + Result = get_ok_value([data, server, getLoglevel], get_loglevel(Config)), + ?assert(is_binary(Result)). + +get_status_test(Config) -> + Result = get_ok_value([data, server, status], get_status(Config)), + ?assertEqual(<<"RUNNING">>, maps:get(<<"statusCode">>, Result)), + ?assert(is_binary(maps:get(<<"message">>, Result))). + +join_successful_prompt(Config) -> + #{node := Node2} = RPCSpec2 = mim2(), + join_cluster(atom_to_binary(Node2), Config), + distributed_helper:verify_result(RPCSpec2, add). + +%leave_successful_prompt(Config) -> +% Node2 = mim2(), +% add_node_to_cluster(Node2, Config), +% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "yes\n", Config), +% distributed_helper:verify_result(Node2, remove), +% ?eq(0, OpCode). +% +%join_unsuccessful(Config) -> +% Node2 = mim2(), +% {_, OpCode} = mongooseimctl_interactive("join_cluster", [], "no\n", Config), +% distributed_helper:verify_result(Node2, remove), +% ?ne(0, OpCode). +% +%leave_unsuccessful(Config) -> +% Node2 = mim(), +% add_node_to_cluster(Node2, Config), +% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "no\n", Config), +% distributed_helper:verify_result(Node2, add), +% ?ne(0, OpCode). +% +%leave_but_no_cluster(Config) -> +% Node2 = mim2(), +% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "yes\n", Config), +% distributed_helper:verify_result(Node2, remove), +% ?ne(0, OpCode). +% +%join_twice(Config) -> +% #{node := Node2} = RPCSpec2 = mim2(), +% {_, OpCode1} = mongooseimctl_interactive("join_cluster", +% [atom_to_list(Node2)], "yes\n", Config), +% {_, OpCode2} = mongooseimctl_interactive("join_cluster", +% [atom_to_list(Node2)], "yes\n", Config), +% distributed_helper:verify_result(RPCSpec2, add), +% ?eq(0, OpCode1), +% ?ne(0, OpCode2). + +get_cookie(Config) -> + execute_command(<<"server">>, <<"getCookie">>, #{}, Config). + +get_loglevel(Config) -> + execute_command(<<"server">>, <<"getLoglevel">>, #{}, Config). + +get_status(Config) -> + execute_command(<<"server">>, <<"status">>, #{}, Config). + +join_cluster(Cluster, Config) -> + execute_command(<<"server">>, <<"joinCluster">>, #{<<"cluster">> => Cluster}, Config). diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 60a39b7cb7c..fd7bf8b9dc2 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -38,6 +38,8 @@ type AdminQuery{ gdpr: GdprAdminQuery "Mnesia internal database management" mnesia: MnesiaAdminQuery + "Server management" + server: ServerAdminQuery } """ @@ -75,4 +77,6 @@ type AdminMutation @protected{ token: TokenAdminMutation "Mnesia internal database management" mnesia: MnesiaAdminMutation + "Server management" + server: ServerAdminMutation } diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql new file mode 100644 index 00000000000..55e9d864858 --- /dev/null +++ b/priv/graphql/schemas/admin/server.gql @@ -0,0 +1,31 @@ +""" +Allow admin to acquire data about the node +""" +type ServerAdminQuery @protected{ + status: Status + @protected(type: GLOBAL) + getLoglevel: String + @protected(type: GLOBAL) + getCookie: String + @protected(type: GLOBAL) +} + +type ServerAdminMutation @protected{ + joinCluster(cluster: String!): String + leaveCluster: String + removeFromCluster: String + restart: String + stop: String + setLoglevel: String + removeNode: String +} + +type Status { + statusCode: StatusCode + message: String +} + +enum StatusCode { + RUNNING + NOT_RUNNING +} \ No newline at end of file diff --git a/src/admin_extra/service_admin_extra_node.erl b/src/admin_extra/service_admin_extra_node.erl index a91f68f10d5..fb6d2cb516f 100644 --- a/src/admin_extra/service_admin_extra_node.erl +++ b/src/admin_extra/service_admin_extra_node.erl @@ -27,7 +27,6 @@ -author('badlop@process-one.net'). -export([commands/0, - get_cookie/0, remove_node/1]). -ignore_xref([ @@ -45,7 +44,7 @@ commands() -> [ #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", - module = ?MODULE, function = get_cookie, + module = server_api, function = get_cookie, args = [], result = {cookie, string}}, #ejabberd_commands{name = remove_node, tags = [erlang], @@ -61,11 +60,6 @@ commands() -> %%% --spec get_cookie() -> string(). -get_cookie() -> - atom_to_list(erlang:get_cookie()). - - -spec remove_node(string()) -> 'ok'. remove_node(Node) -> mnesia:del_table_copy(schema, list_to_atom(Node)), diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index dcb9f2e3987..e406138b45c 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -30,15 +30,13 @@ -export([start/0, stop/0, %% Server - status/0, %% Accounts register/3, register/2, unregister/2, registered_users/1, import_users/1, %% Purge DB delete_expired_messages/1, delete_old_messages/2, - get_loglevel/0, - join_cluster/1, leave_cluster/0, + leave_cluster/0, remove_from_cluster/1]). -export([registrator_proc/1]). @@ -72,7 +70,7 @@ commands() -> %% They are defined here so that other interfaces can use them too #ejabberd_commands{name = status, tags = [server], desc = "Get status of the ejabberd server", - module = ?MODULE, function = status, + module = server_api, function = status, args = [], result = {res, restuple}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", @@ -80,7 +78,7 @@ commands() -> args = [], result = {res, rescode}}, #ejabberd_commands{name = get_loglevel, tags = [logs, server], desc = "Get the current loglevel", - module = ?MODULE, function = get_loglevel, + module = server_api, function = get_loglevel, args = [], result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], @@ -159,7 +157,7 @@ commands() -> #ejabberd_commands{name = join_cluster, tags = [server], desc = "Join the node to a cluster. Call it from the joining node. Use `-f` or `--force` flag to avoid question prompt and force join the node", - module = ?MODULE, function = join_cluster, + module = server_api, function = join_cluster, args = [{node, string}], result = {res, restuple}}, #ejabberd_commands{name = leave_cluster, tags = [server], @@ -221,35 +219,6 @@ remove_rpc_alive_node(AliveNode) -> {rpc_error, String} end. --spec join_cluster(string()) -> {ok, string()} | {pang, string()} | {already_joined, string()} | - {mnesia_error, string()} | {error, string()}. -join_cluster(NodeString) -> - NodeAtom = list_to_atom(NodeString), - NodeList = mnesia:system_info(db_nodes), - case lists:member(NodeAtom, NodeList) of - true -> - String = io_lib:format("The node ~s has already joined the cluster~n", [NodeString]), - {already_joined, String}; - _ -> - do_join_cluster(NodeAtom) - end. - -do_join_cluster(Node) -> - try mongoose_cluster:join(Node) of - ok -> - String = io_lib:format("You have successfully joined the node ~p to the cluster with node member ~p~n", [node(), Node]), - {ok, String} - catch - error:pang -> - String = io_lib:format("Timeout while attempting to connect to node ~s~n", [Node]), - {pang, String}; - error:{cant_get_storage_type, {T, E, R}} -> - String = io_lib:format("Cannot get storage type for table ~p~n. Reason: ~p:~p", [T, E, R]), - {mnesia_error, String}; - E:R:S -> - {error, {E, R, S}} - end. - -spec leave_cluster() -> {ok, string()} | {error, term()} | {not_in_cluster, string()}. leave_cluster() -> NodeList = mnesia:system_info(running_db_nodes), @@ -272,20 +241,6 @@ do_leave_cluster() -> {error, {E, R}} end. --spec status() -> {'mongooseim_not_running', io_lib:chars()} | {'ok', io_lib:chars()}. -status() -> - {InternalStatus, ProvidedStatus} = init:get_status(), - String1 = io_lib:format("The node ~p is ~p. Status: ~p", - [node(), InternalStatus, ProvidedStatus]), - {Is_running, String2} = - case lists:keysearch(mongooseim, 1, application:which_applications()) of - false -> - {mongooseim_not_running, "mongooseim is not running in that node."}; - {value, {_, _, Version}} -> - {ok, io_lib:format("mongooseim ~s is running in that node", [Version])} - end, - {Is_running, String1 ++ String2}. - %%% %%% Account management %%% @@ -397,12 +352,6 @@ do_register(List) -> Info = lists:foldr(JoinBinary, <<"">>, List), {bad_csv, Info}. -get_loglevel() -> - Level = mongoose_logs:get_global_loglevel(), - Number = mongoose_logs:loglevel_keyword_to_number(Level), - String = io_lib:format("global loglevel is ~p, which means '~p'", [Number, Level]), - {ok, String}. - %%% %%% Purge DB %%% diff --git a/src/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index e6fa2b2e19e..42feb23d2d7 100644 --- a/src/graphql/admin/mongoose_graphql_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_admin_mutation.erl @@ -33,5 +33,7 @@ execute(_Ctx, _Obj, <<"vcard">>, _Args) -> {ok, vcard}; execute(_Ctx, _Obj, <<"token">>, _Args) -> {ok, token}; +execute(_Ctx, _Obj, <<"server">>, _Args) -> + {ok, server}; execute(_Ctx, _Obj, <<"mnesia">>, _Args) -> {ok, mnesia}. diff --git a/src/graphql/admin/mongoose_graphql_admin_query.erl b/src/graphql/admin/mongoose_graphql_admin_query.erl index 4b22993e152..faf446b2a2f 100644 --- a/src/graphql/admin/mongoose_graphql_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_admin_query.erl @@ -7,16 +7,20 @@ -include("../mongoose_graphql_types.hrl"). -execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> - {ok, admin}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> + {ok, admin}; execute(_Ctx, _Obj, <<"domain">>, _Args) -> {ok, admin}; execute(_Ctx, _Obj, <<"gdpr">>, _Args) -> {ok, gdpr}; execute(_Ctx, _Obj, <<"last">>, _Args) -> {ok, last}; +execute(_Ctx, _Obj, <<"metric">>, _Args) -> + {ok, metric}; +execute(_Ctx, _Obj, <<"mnesia">>, _Args) -> + {ok, mnesia}; execute(_Ctx, _Obj, <<"muc">>, _Args) -> {ok, muc}; execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> @@ -25,15 +29,13 @@ execute(_Ctx, _Obj, <<"private">>, _Args) -> {ok, private}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; +execute(_Ctx, _Obj, <<"server">>, _Args) -> + {ok, server}; execute(_Ctx, _Obj, <<"session">>, _Args) -> {ok, session}; -execute(_Ctx, _Obj, <<"stat">>, _Args) -> - {ok, stats}; execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, #{}}; +execute(_Ctx, _Obj, <<"stat">>, _Args) -> + {ok, stats}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> - {ok, vcard}; -execute(_Ctx, _Obj, <<"mnesia">>, _Args) -> - {ok, mnesia}; -execute(_Ctx, _Obj, <<"metric">>, _Args) -> - {ok, metric}. + {ok, vcard}. diff --git a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl new file mode 100644 index 00000000000..375347a9b3a --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl @@ -0,0 +1,21 @@ +-module(mongoose_graphql_server_admin_mutation). +-behaviour(mongoose_graphql). + +-export([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2]). +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). + +execute(_Ctx, server, <<"joinCluster">>, #{<<"cluster">> := Cluster}) -> + case server_api:join_cluster(binary_to_list(Cluster)) of + {mnesia_error, _} = Error -> + make_error(Error, #{cluster => Cluster}); + {error, _} = Error -> + make_error(Error, #{cluster => Cluster}); + {pang, String} -> + make_error({timeout_error, String}, #{cluster => Cluster}); + {_, String} -> + {ok, String} + end. diff --git a/src/graphql/admin/mongoose_graphql_server_admin_query.erl b/src/graphql/admin/mongoose_graphql_server_admin_query.erl new file mode 100644 index 00000000000..c0f46905a63 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_server_admin_query.erl @@ -0,0 +1,20 @@ +-module(mongoose_graphql_server_admin_query). +-behaviour(mongoose_graphql). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). + +execute(_Ctx, server, <<"status">>, _) -> + case server_api:status() of + {ok, String} -> + {ok, #{<<"statusCode">> => <<"RUNNING">>, <<"message">> => String}}; + {_, String} -> + {ok, #{<<"statusCode">> => <<"NOT_RUNNING">>, <<"message">> => String}} + end; +execute(_Ctx, server, <<"getLoglevel">>, _) -> + server_api:get_loglevel(); +execute(_Ctx, server, <<"getCookie">>, _) -> + {ok, server_api:get_cookie()}. diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 87771d96ad8..8d8f2fe9d8f 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -142,6 +142,8 @@ admin_mapping_rules() -> 'GlobalStats' => mongoose_graphql_stats_global, 'DomainStats' => mongoose_graphql_stats_domain, 'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, + 'ServerAdminQuery' => mongoose_graphql_server_admin_query, + 'ServerAdminMutation' => mongoose_graphql_server_admin_mutation, 'LastAdminMutation' => mongoose_graphql_last_admin_mutation, 'LastAdminQuery' => mongoose_graphql_last_admin_query, 'AccountAdminQuery' => mongoose_graphql_account_admin_query, diff --git a/src/graphql/mongoose_graphql_enum.erl b/src/graphql/mongoose_graphql_enum.erl index 948184bada6..0da9c442b3d 100644 --- a/src/graphql/mongoose_graphql_enum.erl +++ b/src/graphql/mongoose_graphql_enum.erl @@ -69,4 +69,5 @@ output(<<"AddressTags">>, Name) -> {ok, Name}; output(<<"EmailTags">>, Name) -> {ok, Name}; output(<<"PrivacyClassificationTags">>, Name) -> {ok, Name}; output(<<"TelephoneTags">>, Name) -> {ok, Name}; -output(<<"MetricType">>, Type) -> {ok, Type}. +output(<<"MetricType">>, Type) -> {ok, Type}; +output(<<"StatusCode">>, Code) -> {ok, Code}. diff --git a/src/graphql/mongoose_graphql_errors.erl b/src/graphql/mongoose_graphql_errors.erl index d01b3d2c802..4fa5ee4f4e9 100644 --- a/src/graphql/mongoose_graphql_errors.erl +++ b/src/graphql/mongoose_graphql_errors.erl @@ -44,6 +44,9 @@ err(_Ctx, ErrorTerm) -> %% callback invoked when resolver crashes -spec crash(map(), term()) -> err_msg(). crash(_Ctx, Err = #{type := Type}) -> + io:format("---------------------------------------------------------------------------------------------------------\n"), + io:format("~p\n", [Err]), + io:format("---------------------------------------------------------------------------------------------------------\n"), ?LOG_ERROR(Err#{what => graphql_crash}), #{message => <<"Unexpected ", Type/binary, " resolver crash">>, extensions => #{code => resolver_crash}}. diff --git a/src/server_api.erl b/src/server_api.erl new file mode 100644 index 00000000000..1b6e746cda0 --- /dev/null +++ b/src/server_api.erl @@ -0,0 +1,57 @@ +-module(server_api). + +-export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1]). + +-spec get_loglevel() -> {ok, string()}. +get_loglevel() -> + Level = mongoose_logs:get_global_loglevel(), + Number = mongoose_logs:loglevel_keyword_to_number(Level), + String = io_lib:format("global loglevel is ~p, which means '~p'", [Number, Level]), + {ok, String}. + +-spec status() -> {'mongooseim_not_running', io_lib:chars()} | {'ok', io_lib:chars()}. +status() -> + {InternalStatus, ProvidedStatus} = init:get_status(), + String1 = io_lib:format("The node ~p is ~p. Status: ~p", + [node(), InternalStatus, ProvidedStatus]), + case lists:keysearch(mongooseim, 1, application:which_applications()) of + false -> + {mongooseim_not_running, String1 ++ "mongooseim is not running in that node."}; + {value, {_, _, Version}} -> + {ok, String1 ++ io_lib:format("mongooseim ~s is running in that node", [Version])} + end. + +-spec get_cookie() -> string(). +get_cookie() -> + atom_to_list(erlang:get_cookie()). + +-spec join_cluster(string()) -> {ok, string()} | {pang, string()} | {already_joined, string()} | + {mnesia_error, string()} | {error, string()}. +join_cluster(NodeString) -> + NodeAtom = list_to_atom(NodeString), + NodeList = mnesia:system_info(db_nodes), + case lists:member(NodeAtom, NodeList) of + true -> + String = io_lib:format("The node ~s has already joined the cluster~n", [NodeString]), + {already_joined, String}; + _ -> + do_join_cluster(NodeAtom) + end. + +do_join_cluster(Node) -> + try mongoose_cluster:join(Node) of + ok -> + String = io_lib:format("You have successfully joined the node" + ++ " ~p to the cluster with node member ~p~n", [node(), Node]), + {ok, String} + catch + error:pang -> + String = io_lib:format("Timeout while attempting to connect to node ~s~n", [Node]), + {pang, String}; + error:{cant_get_storage_type, {T, E, R}} -> + String = + io_lib:format("Cannot get storage type for table ~p~n. Reason: ~p:~p", [T, E, R]), + {mnesia_error, String}; + E:R:S -> + {error, {E, R, S}} + end. diff --git a/test/mongoose_config_SUITE.erl b/test/mongoose_config_SUITE.erl index 65915c35d10..a19d79c3b42 100644 --- a/test/mongoose_config_SUITE.erl +++ b/test/mongoose_config_SUITE.erl @@ -244,7 +244,7 @@ code_paths() -> [filename:absname(Path) || Path <- code:get_path()]. maybe_join_cluster(SlaveNode) -> - Result = rpc:call(SlaveNode, ejabberd_admin, join_cluster, + Result = rpc:call(SlaveNode, server_api, join_cluster, [atom_to_list(node())]), case Result of {ok, _} -> From 26598b6c0ffbf0507e5b1c8792741d1d26f60d7d Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Mon, 5 Sep 2022 08:08:13 +0200 Subject: [PATCH 030/117] Adding graphql_server_admin_queries --- big_tests/tests/cluster_commands_SUITE.erl | 2 +- big_tests/tests/graphql_server_SUITE.erl | 79 +++++++++---------- priv/graphql/schemas/admin/server.gql | 2 +- src/ejabberd_admin.erl | 28 +------ ...mongoose_graphql_server_admin_mutation.erl | 19 +++-- src/server_api.erl | 24 +++++- 6 files changed, 78 insertions(+), 76 deletions(-) diff --git a/big_tests/tests/cluster_commands_SUITE.erl b/big_tests/tests/cluster_commands_SUITE.erl index ac058dd1f70..54592c63ea7 100644 --- a/big_tests/tests/cluster_commands_SUITE.erl +++ b/big_tests/tests/cluster_commands_SUITE.erl @@ -332,7 +332,7 @@ leave_using_rpc(Config) -> add_node_to_cluster(Node2, Config), %% when Result = distributed_helper:rpc(Node1#{timeout => timer:seconds(30)}, - ejabberd_admin, leave_cluster, []), + server_api, leave_cluster, []), ct:pal("leave_using_rpc result ~p~n", [Result]), %% then distributed_helper:verify_result(Node2, remove), diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl index 989b96c4f90..adec3d059c3 100644 --- a/big_tests/tests/graphql_server_SUITE.erl +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -15,8 +15,8 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, admin_http}, - {group, admin_cli}]. + [{group, admin_cli}, + {group, admin_http}]. groups() -> [{admin_http, [], admin_groups()}, @@ -31,11 +31,14 @@ admin_groups() -> admin_tests() -> [get_cookie_test, get_loglevel_test, - get_status_test, - set_loglevel_test]. + get_status_test]. clustering_tests() -> - [get_status_test, join_successful_prompt]. + [join_successful, + leave_successful, + join_unsuccessful, + leave_but_no_cluster, + join_twice]. init_per_suite(Config) -> Config1 = dynamic_modules:save_modules(host_type(), Config), @@ -95,46 +98,33 @@ get_status_test(Config) -> ?assertEqual(<<"RUNNING">>, maps:get(<<"statusCode">>, Result)), ?assert(is_binary(maps:get(<<"message">>, Result))). -join_successful_prompt(Config) -> +join_successful(Config) -> #{node := Node2} = RPCSpec2 = mim2(), + leave_cluster(Config), join_cluster(atom_to_binary(Node2), Config), distributed_helper:verify_result(RPCSpec2, add). -%leave_successful_prompt(Config) -> -% Node2 = mim2(), -% add_node_to_cluster(Node2, Config), -% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "yes\n", Config), -% distributed_helper:verify_result(Node2, remove), -% ?eq(0, OpCode). -% -%join_unsuccessful(Config) -> -% Node2 = mim2(), -% {_, OpCode} = mongooseimctl_interactive("join_cluster", [], "no\n", Config), -% distributed_helper:verify_result(Node2, remove), -% ?ne(0, OpCode). -% -%leave_unsuccessful(Config) -> -% Node2 = mim(), -% add_node_to_cluster(Node2, Config), -% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "no\n", Config), -% distributed_helper:verify_result(Node2, add), -% ?ne(0, OpCode). -% -%leave_but_no_cluster(Config) -> -% Node2 = mim2(), -% {_, OpCode} = mongooseimctl_interactive("leave_cluster", [], "yes\n", Config), -% distributed_helper:verify_result(Node2, remove), -% ?ne(0, OpCode). -% -%join_twice(Config) -> -% #{node := Node2} = RPCSpec2 = mim2(), -% {_, OpCode1} = mongooseimctl_interactive("join_cluster", -% [atom_to_list(Node2)], "yes\n", Config), -% {_, OpCode2} = mongooseimctl_interactive("join_cluster", -% [atom_to_list(Node2)], "yes\n", Config), -% distributed_helper:verify_result(RPCSpec2, add), -% ?eq(0, OpCode1), -% ?ne(0, OpCode2). +leave_successful(Config) -> + #{node := Node2} = RPCSpec2 = mim2(), + join_cluster(atom_to_binary(Node2), Config), + leave_cluster(Config), + distributed_helper:verify_result(RPCSpec2, remove). + +join_unsuccessful(Config) -> + Node2 = mim2(), + join_cluster(<<>>, Config), + distributed_helper:verify_result(Node2, remove). + +leave_but_no_cluster(Config) -> + Node2 = mim2(), + leave_cluster(Config), + distributed_helper:verify_result(Node2, remove). + +join_twice(Config) -> + #{node := Node2} = RPCSpec2 = mim2(), + join_cluster(atom_to_binary(Node2), Config), + join_cluster(atom_to_binary(Node2), Config), + distributed_helper:verify_result(RPCSpec2, add). get_cookie(Config) -> execute_command(<<"server">>, <<"getCookie">>, #{}, Config). @@ -145,5 +135,8 @@ get_loglevel(Config) -> get_status(Config) -> execute_command(<<"server">>, <<"status">>, #{}, Config). -join_cluster(Cluster, Config) -> - execute_command(<<"server">>, <<"joinCluster">>, #{<<"cluster">> => Cluster}, Config). +join_cluster(Node, Config) -> + execute_command(<<"server">>, <<"joinCluster">>, #{<<"node">> => Node}, Config). + +leave_cluster(Config) -> + execute_command(<<"server">>, <<"leaveCluster">>, #{}, Config). diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index 55e9d864858..02b9e64281c 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -11,7 +11,7 @@ type ServerAdminQuery @protected{ } type ServerAdminMutation @protected{ - joinCluster(cluster: String!): String + joinCluster(node: String!): String leaveCluster: String removeFromCluster: String restart: String diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index e406138b45c..613468c4c7f 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -36,7 +36,6 @@ import_users/1, %% Purge DB delete_expired_messages/1, delete_old_messages/2, - leave_cluster/0, remove_from_cluster/1]). -export([registrator_proc/1]). @@ -44,8 +43,8 @@ -ignore_xref([ backup_mnesia/1, delete_expired_messages/1, delete_old_messages/2, dump_mnesia/1, dump_table/2, - get_loglevel/0, import_users/1, install_fallback_mnesia/1, - join_cluster/1, leave_cluster/0, load_mnesia/1, mnesia_change_nodename/4, + import_users/1, install_fallback_mnesia/1, + load_mnesia/1, mnesia_change_nodename/4, register/2, register/3, registered_users/1, remove_from_cluster/1, restore_mnesia/1, status/0, stop/0, unregister/2]). @@ -163,7 +162,7 @@ commands() -> #ejabberd_commands{name = leave_cluster, tags = [server], desc = "Leave a cluster. Call it from the node that is going to leave. Use `-f` or `--force` flag to avoid question prompt and force leave the node from cluster", - module = ?MODULE, function = leave_cluster, + module = server_api, function = leave_cluster, args = [], result = {res, restuple}}, #ejabberd_commands{name = remove_from_cluster, tags = [server], @@ -219,27 +218,6 @@ remove_rpc_alive_node(AliveNode) -> {rpc_error, String} end. --spec leave_cluster() -> {ok, string()} | {error, term()} | {not_in_cluster, string()}. -leave_cluster() -> - NodeList = mnesia:system_info(running_db_nodes), - ThisNode = node(), - case NodeList of - [ThisNode] -> - String = io_lib:format("The node ~p is not in the cluster~n", [node()]), - {not_in_cluster, String}; - _ -> - do_leave_cluster() - end. - -do_leave_cluster() -> - try mongoose_cluster:leave() of - ok -> - String = io_lib:format("The node ~p has successfully left the cluster~n", [node()]), - {ok, String} - catch - E:R -> - {error, {E, R}} - end. %%% %%% Account management diff --git a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl index 375347a9b3a..b2a8492b055 100644 --- a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl @@ -8,14 +8,23 @@ -include("../mongoose_graphql_types.hrl"). -execute(_Ctx, server, <<"joinCluster">>, #{<<"cluster">> := Cluster}) -> - case server_api:join_cluster(binary_to_list(Cluster)) of +execute(_Ctx, server, <<"joinCluster">>, #{<<"node">> := Node}) -> + case server_api:join_cluster(binary_to_list(Node)) of {mnesia_error, _} = Error -> - make_error(Error, #{cluster => Cluster}); + make_error(Error, #{cluster => Node}); {error, _} = Error -> - make_error(Error, #{cluster => Cluster}); + make_error(Error, #{cluster => Node}); {pang, String} -> - make_error({timeout_error, String}, #{cluster => Cluster}); + make_error({timeout_error, String}, #{cluster => Node}); + {_, String} -> + {ok, String} + end; +execute(_Ctx, server, <<"leaveCluster">>, #{}) -> + case server_api:leave_cluster() of + {error, _} = Error -> + make_error(Error, #{}); + {not_in_cluster, String} -> + make_error({not_in_cluster_error, String}, #{}); {_, String} -> {ok, String} end. diff --git a/src/server_api.erl b/src/server_api.erl index 1b6e746cda0..6e33c475be3 100644 --- a/src/server_api.erl +++ b/src/server_api.erl @@ -1,6 +1,6 @@ -module(server_api). --export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1]). +-export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1, leave_cluster/0]). -spec get_loglevel() -> {ok, string()}. get_loglevel() -> @@ -55,3 +55,25 @@ do_join_cluster(Node) -> E:R:S -> {error, {E, R, S}} end. + +-spec leave_cluster() -> {ok, string()} | {error, term()} | {not_in_cluster, string()}. +leave_cluster() -> + NodeList = mnesia:system_info(running_db_nodes), + ThisNode = node(), + case NodeList of + [ThisNode] -> + String = io_lib:format("The node ~p is not in the cluster~n", [node()]), + {not_in_cluster, String}; + _ -> + do_leave_cluster() + end. + +do_leave_cluster() -> + try mongoose_cluster:leave() of + ok -> + String = io_lib:format("The node ~p has successfully left the cluster~n", [node()]), + {ok, String} + catch + E:R -> + {error, {E, R}} + end. From 9b6aa1ad632569a555fef5f3d4bf239e83255c8a Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 7 Sep 2022 11:36:51 +0200 Subject: [PATCH 031/117] Adding stop and restart commands --- big_tests/tests/cluster_commands_SUITE.erl | 4 +- big_tests/tests/distributed_helper.erl | 2 +- big_tests/tests/graphql_helper.erl | 49 +++++--- big_tests/tests/graphql_server_SUITE.erl | 115 +++++++++++++++--- priv/graphql/schemas/admin/server.gql | 5 +- src/ejabberd_admin.erl | 2 +- ...mongoose_graphql_server_admin_mutation.erl | 15 ++- src/server_api.erl | 52 +++++++- 8 files changed, 203 insertions(+), 41 deletions(-) diff --git a/big_tests/tests/cluster_commands_SUITE.erl b/big_tests/tests/cluster_commands_SUITE.erl index 54592c63ea7..3020b5af1ff 100644 --- a/big_tests/tests/cluster_commands_SUITE.erl +++ b/big_tests/tests/cluster_commands_SUITE.erl @@ -395,7 +395,7 @@ remove_dead_from_cluster(Config) -> ok = rpc(Node2#{timeout => Timeout}, mongoose_cluster, join, [Node1Nodename]), ok = rpc(Node3#{timeout => Timeout}, mongoose_cluster, join, [Node1Nodename]), %% when - distributed_helper:stop_node(Node3, Config), + distributed_helper:stop_node(Node3Nodename, Config), {_, OpCode1} = mongooseimctl_interactive(Node1, "remove_from_cluster", [atom_to_list(Node3Nodename)], "yes\n", Config), %% then @@ -405,7 +405,7 @@ remove_dead_from_cluster(Config) -> have_node_in_mnesia(Node1, Node3, false), have_node_in_mnesia(Node2, Node3, false), % after node awakening nodes are clustered again - distributed_helper:start_node(Node3, Config), + distributed_helper:start_node(Node3Nodename, Config), have_node_in_mnesia(Node1, Node3, true), have_node_in_mnesia(Node2, Node3, true). diff --git a/big_tests/tests/distributed_helper.erl b/big_tests/tests/distributed_helper.erl index a71f4b41817..d3623f9a291 100644 --- a/big_tests/tests/distributed_helper.erl +++ b/big_tests/tests/distributed_helper.erl @@ -168,7 +168,7 @@ get_or_fail(Key) -> start_node(Node, Config) -> {_, 0} = mongooseimctl_helper:mongooseimctl(Node, "start", [], Config), - {_, 0} = mongooseimctl_helper:mongooseimctl(Node, "started", [], Config), + %{_, 0} = mongooseimctl_helper:mongooseimctl(Node, "started", [], Config), %% TODO Looks like "started" run by mongooseimctl fun is not really synchronous timer:sleep(3000). diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index ea260715fc0..23643bd1ba6 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -8,11 +8,11 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("escalus/include/escalus.hrl"). --spec execute(atom(), binary(), {binary(), binary()} | undefined) -> +-spec execute(node(), atom(), binary(), {binary(), binary()} | undefined) -> {Status :: tuple(), Data :: map()}. -execute(EpName, Body, Creds) -> +execute(Node, EpName, Body, Creds) -> Request = - #{port => get_listener_port(EpName), + #{port => get_listener_port(Node, EpName), role => {graphql, EpName}, method => <<"POST">>, return_maps => true, @@ -26,21 +26,26 @@ execute_user_command(Category, Command, User, Args, Config) -> execute_user(#{query => Doc, variables => Args}, User, Config). execute_command(Category, Command, Args, Config) -> + #{node := Node} = mim(), Protocol = ?config(protocol, Config), - execute_command(Category, Command, Args, Config, Protocol). + execute_command(Node, Category, Command, Args, Config, Protocol). + +execute_command(Node, Category, Command, Args, Config) -> + Protocol = ?config(protocol, Config), + execute_command(Node, Category, Command, Args, Config, Protocol). %% Admin commands can be executed as GraphQL over HTTP or with CLI (mongooseimctl) -execute_command(Category, Command, Args, Config, http) -> +execute_command(Node, Category, Command, Args, Config, http) -> #{Category := #{commands := #{Command := #{doc := Doc}}}} = get_specs(), - execute_auth(#{query => Doc, variables => Args}, Config); -execute_command(Category, Command, Args, Config, cli) -> + execute_auth(Node, #{query => Doc, variables => Args}, Config); +execute_command(Node, Category, Command, Args, Config, cli) -> CLIArgs = encode_cli_args(Args), - {Result, Code} = mongooseimctl_helper:mongooseimctl(Category, [Command | CLIArgs], Config), + {Result, Code} + = mongooseimctl_helper:mongooseimctl(Node, Category, [Command | CLIArgs], Config), {{exit_status, Code}, rest_helper:decode(Result, #{return_maps => true})}. encode_cli_args(Args) -> lists:flatmap(fun({Name, Value}) -> encode_cli_arg(Name, Value) end, maps:to_list(Args)). - encode_cli_arg(_Name, null) -> []; encode_cli_arg(Name, Value) -> @@ -56,28 +61,33 @@ arg_value_to_binary(Value) when is_list(Value); is_map(Value) -> iolist_to_binary(jiffy:encode(Value)). execute_auth(Body, Config) -> + #{node := Node} = mim(), + execute_auth(Node, Body, Config). + +execute_auth(Node, Body, Config) -> case Ep = ?config(schema_endpoint, Config) of admin -> #{username := Username, password := Password} = get_listener_opts(Ep), - execute(Ep, Body, {Username, Password}); + execute(Node, Ep, Body, {Username, Password}); domain_admin -> Creds = ?config(domain_admin, Config), - execute(Ep, Body, Creds) + execute(Node, Ep, Body, Creds) end. execute_user(Body, User, Config) -> Ep = ?config(schema_endpoint, Config), Creds = make_creds(User), - execute(Ep, Body, Creds). + #{node := Node} = mim(), + execute(Node, Ep, Body, Creds). --spec get_listener_port(binary()) -> integer(). -get_listener_port(EpName) -> - #{port := Port} = get_listener_config(EpName), +-spec get_listener_port(node(), binary()) -> integer(). +get_listener_port(Node, EpName) -> + #{port := Port} = get_listener_config(Node, EpName), Port. --spec get_listener_config(binary()) -> map(). -get_listener_config(EpName) -> - Listeners = rpc(mim(), mongoose_config, get_opt, [listen]), +-spec get_listener_config(node(), binary()) -> map(). +get_listener_config(Node, EpName) -> + Listeners = rpc(#{node => Node}, mongoose_config, get_opt, [listen]), [Config] = lists:filter(fun(Config) -> is_graphql_config(Config, EpName) end, Listeners), Config. @@ -129,7 +139,8 @@ end_domain_admin_handler(Config) -> domain_helper:delete_domain_password(mim(), Domain). get_listener_opts(EpName) -> - #{handlers := [Opts]} = get_listener_config(EpName), + #{node := Node} = mim(), + #{handlers := [Opts]} = get_listener_config(Node, EpName), Opts. get_err_code(Resp) -> diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl index adec3d059c3..685829c1db0 100644 --- a/big_tests/tests/graphql_server_SUITE.erl +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -4,10 +4,11 @@ -import(distributed_helper, [is_sm_distributed/0, mim/0, mim2/0, mim3/0, - require_rpc_nodes/1]). + remove_node_from_cluster/2, + require_rpc_nodes/1, rpc/4]). -import(domain_helper, [host_type/0, domain/0]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, - get_err_msg/1, get_err_code/1]). + get_err_msg/1, get_err_code/1, execute_command/5]). -include_lib("eunit/include/eunit.hrl"). @@ -15,18 +16,18 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, admin_cli}, - {group, admin_http}]. + [{group, admin_http}, + {group, admin_cli}]. groups() -> [{admin_http, [], admin_groups()}, {admin_cli, [], admin_groups()}, {server_tests, [], admin_tests()}, - {clustering_two_tests, [], clustering_tests()}]. + {clustering_tests, [], clustering_tests()}]. admin_groups() -> [{group, server_tests}, - {group, clustering_two_tests}]. + {group, clustering_tests}]. admin_tests() -> [get_cookie_test, @@ -38,7 +39,10 @@ clustering_tests() -> leave_successful, join_unsuccessful, leave_but_no_cluster, - join_twice]. + join_twice, + remove_dead_from_cluster, + remove_alive_from_cluster, + stop_node_test]. init_per_suite(Config) -> Config1 = dynamic_modules:save_modules(host_type(), Config), @@ -54,7 +58,7 @@ init_per_suite(Config) -> escalus:init_per_suite([{ctl_path_atom(Node1), NodeCtlPath}, {ctl_path_atom(Node2), Node2CtlPath}, {ctl_path_atom(Node3), Node3CtlPath}] - ++ Config3). + ++ Config4). ctl_path_atom(NodeName) -> CtlString = atom_to_list(NodeName) ++ "_ctl", @@ -68,7 +72,7 @@ init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); -init_per_group(clustering_two_tests, Config) -> +init_per_group(clustering_tests, Config) -> case is_sm_distributed() of true -> Config; @@ -85,6 +89,25 @@ end_per_group(Group, _Config) when Group =:= admin_http; end_per_group(_, _Config) -> escalus_fresh:clean(). +init_per_testcase(CaseName, Config) -> + escalus:init_per_testcase(CaseName, Config). + +end_per_testcase(CaseName, Config) when CaseName == join_successful + orelse CaseName == leave_unsuccessful + orelse CaseName == join_twice -> + Node2 = mim2(), + remove_node_from_cluster(Node2, Config), + escalus:end_per_testcase(CaseName, Config); +end_per_testcase(CaseName, Config) when CaseName == remove_alive_from_cluster + orelse CaseName == remove_dead_from_cluster-> + Node3 = mim3(), + Node2 = mim2(), + remove_node_from_cluster(Node3, Config), + remove_node_from_cluster(Node2, Config), + escalus:end_per_testcase(CaseName, Config); +end_per_testcase(CaseName, Config) -> + escalus:end_per_testcase(CaseName, Config). + get_cookie_test(Config) -> Result = get_ok_value([data, server, getCookie], get_cookie(Config)), ?assert(is_binary(Result)). @@ -101,13 +124,13 @@ get_status_test(Config) -> join_successful(Config) -> #{node := Node2} = RPCSpec2 = mim2(), leave_cluster(Config), - join_cluster(atom_to_binary(Node2), Config), + get_ok_value([], join_cluster(atom_to_binary(Node2), Config)), distributed_helper:verify_result(RPCSpec2, add). leave_successful(Config) -> #{node := Node2} = RPCSpec2 = mim2(), join_cluster(atom_to_binary(Node2), Config), - leave_cluster(Config), + get_ok_value([], leave_cluster(Config)), distributed_helper:verify_result(RPCSpec2, remove). join_unsuccessful(Config) -> @@ -117,15 +140,70 @@ join_unsuccessful(Config) -> leave_but_no_cluster(Config) -> Node2 = mim2(), - leave_cluster(Config), + get_err_code(leave_cluster(Config)), distributed_helper:verify_result(Node2, remove). join_twice(Config) -> #{node := Node2} = RPCSpec2 = mim2(), - join_cluster(atom_to_binary(Node2), Config), - join_cluster(atom_to_binary(Node2), Config), + get_ok_value([], join_cluster(atom_to_binary(Node2), Config)), + get_ok_value([], join_cluster(atom_to_binary(Node2), Config)), distributed_helper:verify_result(RPCSpec2, add). +remove_dead_from_cluster(Config) -> + % given + Timeout = timer:seconds(60), + #{node := Node1Nodename} = Node1 = mim(), + #{node := _Node2Nodename} = Node2 = mim2(), + #{node := Node3Nodename} = Node3 = mim3(), + ok = rpc(Node2#{timeout => Timeout}, mongoose_cluster, join, [Node1Nodename]), + ok = rpc(Node3#{timeout => Timeout}, mongoose_cluster, join, [Node1Nodename]), + %% when + distributed_helper:stop_node(Node3Nodename, Config), + get_ok_value([data, server, removeFromCluster], + remove_from_cluster(atom_to_binary(Node3Nodename), Config)), + %% then + % node is down hence its not in mnesia cluster + have_node_in_mnesia(Node1, Node2, true), + have_node_in_mnesia(Node1, Node3, false), + have_node_in_mnesia(Node2, Node3, false), + % after node awakening nodes are clustered again + distributed_helper:start_node(Node3Nodename, Config), + have_node_in_mnesia(Node1, Node3, true), + have_node_in_mnesia(Node2, Node3, true). + +remove_alive_from_cluster(Config) -> + % given + Timeout = timer:seconds(60), + #{node := Node1Name} = Node1 = mim(), + #{node := Node2Name} = Node2 = mim2(), + Node3 = mim3(), + ok = rpc(Node2#{timeout => Timeout}, mongoose_cluster, join, [Node1Name]), + ok = rpc(Node3#{timeout => Timeout}, mongoose_cluster, join, [Node1Name]), + %% when + %% Node2 is still running + %% then + get_ok_value([], remove_from_cluster(atom_to_binary(Node2Name), Config)), + % node is down hence its not in mnesia cluster + have_node_in_mnesia(Node1, Node3, true), + have_node_in_mnesia(Node1, Node2, false), + have_node_in_mnesia(Node3, Node2, false). + +stop_node_test(Config) -> + #{node := Node1Name} = mim(), + #{node := Node3Nodename} = Node3 = mim3(), + get_ok_value([data, server, stop], stop_node(Node3Nodename, Config)), + Timeout = timer:seconds(3), + {badrpc, nodedown} = rpc:call(Node3Nodename, mongoose_cluster, join, [Node1Name], Timeout), + distributed_helper:start_node(Node3Nodename, Config). + +%----------------------------------------------------------------------- +% Helpers +%----------------------------------------------------------------------- + +have_node_in_mnesia(Node1, #{node := Node2}, ShouldBe) -> + DbNodes1 = distributed_helper:rpc(Node1, mnesia, system_info, [db_nodes]), + ?assertEqual(ShouldBe, lists:member(Node2, DbNodes1)). + get_cookie(Config) -> execute_command(<<"server">>, <<"getCookie">>, #{}, Config). @@ -140,3 +218,12 @@ join_cluster(Node, Config) -> leave_cluster(Config) -> execute_command(<<"server">>, <<"leaveCluster">>, #{}, Config). + +remove_from_cluster(Node, Config) -> + execute_command(<<"server">>, <<"removeFromCluster">>, #{<<"node">> => Node}, Config). + +stop_node(Node, Config) -> + execute_command(Node, <<"server">>, <<"stop">>, #{}, Config). + +restart_node(Node, Config) -> + execute_command(Node, <<"server">>, <<"restart">>, #{}, Config). diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index 02b9e64281c..e2ba16a441b 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -13,11 +13,12 @@ type ServerAdminQuery @protected{ type ServerAdminMutation @protected{ joinCluster(node: String!): String leaveCluster: String - removeFromCluster: String + removeFromCluster(node: String): String restart: String stop: String - setLoglevel: String + removeNode: String + setLoglevel: String } type Status { diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 613468c4c7f..b80258cbdbf 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -168,7 +168,7 @@ commands() -> #ejabberd_commands{name = remove_from_cluster, tags = [server], desc = "Remove dead node from the cluster. Call it from the member of the cluster. Use `-f` or `--force` flag to avoid question prompt and force remove the node", - module = ?MODULE, function = remove_from_cluster, + module = server_api, function = remove_from_cluster, args = [{node, string}], result = {res, restuple}} ]. diff --git a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl index b2a8492b055..424e4622117 100644 --- a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl @@ -19,6 +19,13 @@ execute(_Ctx, server, <<"joinCluster">>, #{<<"node">> := Node}) -> {_, String} -> {ok, String} end; +execute(_Ctx, server, <<"removeFromCluster">>, #{<<"node">> := Node}) -> + case server_api:remove_from_cluster(binary_to_list(Node)) of + {ok, _} = Result -> + Result; + Error -> + make_error(Error, #{node => Node}) + end; execute(_Ctx, server, <<"leaveCluster">>, #{}) -> case server_api:leave_cluster() of {error, _} = Error -> @@ -27,4 +34,10 @@ execute(_Ctx, server, <<"leaveCluster">>, #{}) -> make_error({not_in_cluster_error, String}, #{}); {_, String} -> {ok, String} - end. + end; +execute(_Ctx, server, <<"stop">>, #{}) -> + spawn(server_api, stop, []), + {ok, "Stop scheduled"}; +execute(_Ctx, server, <<"restart">>, #{}) -> + spawn(server_api, restart, []), + {ok, "Restart scheduled"}. diff --git a/src/server_api.erl b/src/server_api.erl index 6e33c475be3..8aed97ad092 100644 --- a/src/server_api.erl +++ b/src/server_api.erl @@ -1,6 +1,7 @@ -module(server_api). --export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1, leave_cluster/0]). +-export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1, leave_cluster/0, + remove_from_cluster/1, stop/0, restart/0]). -spec get_loglevel() -> {ok, string()}. get_loglevel() -> @@ -77,3 +78,52 @@ do_leave_cluster() -> E:R -> {error, {E, R}} end. + +-spec remove_from_cluster(string()) -> {ok, string()} | + {node_is_alive, string()} | + {mnesia_error, string()} | + {rpc_error, string()}. +remove_from_cluster(NodeString) -> + Node = list_to_atom(NodeString), + IsNodeAlive = mongoose_cluster:is_node_alive(Node), + case IsNodeAlive of + true -> + remove_rpc_alive_node(Node); + false -> + remove_dead_node(Node) + end. + +remove_dead_node(DeadNode) -> + try mongoose_cluster:remove_from_cluster(DeadNode) of + ok -> + String = io_lib:format("The dead node ~p has been removed from the cluster~n", [DeadNode]), + {ok, String} + catch + error:{node_is_alive, DeadNode} -> + String = io_lib:format("The node ~p is alive but shoud not be.~n", [DeadNode]), + {node_is_alive, String}; + error:{del_table_copy_schema, R} -> + String = io_lib:format("Cannot delete table schema~n. Reason: ~p", [R]), + {mnesia_error, String} + end. + +remove_rpc_alive_node(AliveNode) -> + case rpc:call(AliveNode, mongoose_cluster, leave, []) of + {badrpc, Reason} -> + String = io_lib:format("Cannot remove the node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), + {rpc_error, String}; + ok -> + String = io_lib:format("The node ~p has been removed from the cluster~n", [AliveNode]), + {ok, String}; + Unknown -> + String = io_lib:format("Unknown error: ~p~n", [Unknown]), + {rpc_error, String} + end. + +stop() -> + timer:sleep(500), + init:stop(). + +restart() -> + timer:sleep(500), + init:restart(). From b48594c13d808a35b523538686273a5c4dbd51d5 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Thu, 8 Sep 2022 10:45:10 +0200 Subject: [PATCH 032/117] Adding new graphql category, server --- big_tests/tests/cluster_commands_SUITE.erl | 2 +- big_tests/tests/distributed_helper.erl | 2 - big_tests/tests/graphql_helper.erl | 11 +++++ big_tests/tests/graphql_server_SUITE.erl | 45 ++++++++++++++---- priv/graphql/schemas/admin/server.gql | 44 ++++++++++++++--- src/admin_extra/service_admin_extra_node.erl | 23 ++------- src/ejabberd_admin.erl | 10 ++-- ...mongoose_graphql_server_admin_mutation.erl | 23 +++++---- .../mongoose_graphql_server_admin_query.erl | 6 +-- src/graphql/mongoose_graphql_enum.erl | 2 + src/graphql/mongoose_graphql_errors.erl | 3 -- src/logger/mongoose_logs.erl | 2 + ...server_api.erl => mongoose_server_api.erl} | 39 ++++++++++++--- test/.DS_Store | Bin 0 -> 14340 bytes test/mongoose_config_SUITE.erl | 2 +- 15 files changed, 149 insertions(+), 65 deletions(-) rename src/{server_api.erl => mongoose_server_api.erl} (79%) create mode 100644 test/.DS_Store diff --git a/big_tests/tests/cluster_commands_SUITE.erl b/big_tests/tests/cluster_commands_SUITE.erl index 3020b5af1ff..1e1e3bcc1c7 100644 --- a/big_tests/tests/cluster_commands_SUITE.erl +++ b/big_tests/tests/cluster_commands_SUITE.erl @@ -332,7 +332,7 @@ leave_using_rpc(Config) -> add_node_to_cluster(Node2, Config), %% when Result = distributed_helper:rpc(Node1#{timeout => timer:seconds(30)}, - server_api, leave_cluster, []), + mongoose_server_api, leave_cluster, []), ct:pal("leave_using_rpc result ~p~n", [Result]), %% then distributed_helper:verify_result(Node2, remove), diff --git a/big_tests/tests/distributed_helper.erl b/big_tests/tests/distributed_helper.erl index d3623f9a291..73783392fc2 100644 --- a/big_tests/tests/distributed_helper.erl +++ b/big_tests/tests/distributed_helper.erl @@ -168,8 +168,6 @@ get_or_fail(Key) -> start_node(Node, Config) -> {_, 0} = mongooseimctl_helper:mongooseimctl(Node, "start", [], Config), - %{_, 0} = mongooseimctl_helper:mongooseimctl(Node, "started", [], Config), - %% TODO Looks like "started" run by mongooseimctl fun is not really synchronous timer:sleep(3000). stop_node(Node, Config) -> diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index 23643bd1ba6..34476231c7d 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -8,6 +8,12 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("escalus/include/escalus.hrl"). +-spec execute(atom(), binary(), {binary(), binary()} | undefined) -> + {Status :: tuple(), Data :: map()}. +execute(EpName, Body, Creds) -> + #{node := Node} = mim(), + execute(Node, EpName, Body, Creds). + -spec execute(node(), atom(), binary(), {binary(), binary()} | undefined) -> {Status :: tuple(), Data :: map()}. execute(Node, EpName, Body, Creds) -> @@ -80,6 +86,11 @@ execute_user(Body, User, Config) -> #{node := Node} = mim(), execute(Node, Ep, Body, Creds). +-spec get_listener_port(binary()) -> integer(). +get_listener_port(EpName) -> + #{node := Node} = mim(), + get_listener_port(Node, EpName). + -spec get_listener_port(node(), binary()) -> integer(). get_listener_port(Node, EpName) -> #{port := Port} = get_listener_config(Node, EpName), diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl index 685829c1db0..d75752116de 100644 --- a/big_tests/tests/graphql_server_SUITE.erl +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -16,7 +16,7 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, admin_http}, + [%{group, admin_http}, {group, admin_cli}]. groups() -> @@ -31,7 +31,7 @@ admin_groups() -> admin_tests() -> [get_cookie_test, - get_loglevel_test, + set_and_get_loglevel_test, get_status_test]. clustering_tests() -> @@ -42,6 +42,7 @@ clustering_tests() -> join_twice, remove_dead_from_cluster, remove_alive_from_cluster, + remove_node_test, stop_node_test]. init_per_suite(Config) -> @@ -99,7 +100,7 @@ end_per_testcase(CaseName, Config) when CaseName == join_successful remove_node_from_cluster(Node2, Config), escalus:end_per_testcase(CaseName, Config); end_per_testcase(CaseName, Config) when CaseName == remove_alive_from_cluster - orelse CaseName == remove_dead_from_cluster-> + orelse CaseName == remove_dead_from_cluster -> Node3 = mim3(), Node2 = mim2(), remove_node_from_cluster(Node3, Config), @@ -112,9 +113,24 @@ get_cookie_test(Config) -> Result = get_ok_value([data, server, getCookie], get_cookie(Config)), ?assert(is_binary(Result)). -get_loglevel_test(Config) -> - Result = get_ok_value([data, server, getLoglevel], get_loglevel(Config)), - ?assert(is_binary(Result)). +set_and_get_loglevel_test(Config) -> + LogLevels = [<<"NONE">>, + <<"EMERGENCY">>, + <<"ALERT">>, + <<"CRITICAL">>, + <<"ERROR">>, + <<"WARNING">>, + <<"NOTICE">>, + <<"INFO">>, + <<"DEBUG">>, + <<"ALL">>], + lists:foreach(fun(LogLevel) -> + Value = get_ok_value([data, server, setLoglevel], set_loglevel(LogLevel, Config)), + ?assertEqual(<<"Log level successfully set">>, Value), + Value1 = get_ok_value([data, server, getLoglevel], get_loglevel(Config)), + ?assertEqual(LogLevel, Value1) + end, LogLevels), + ?assertEqual(<<"unknown_enum">>, get_err_code(set_loglevel(<<"AAAA">>, Config))). get_status_test(Config) -> Result = get_ok_value([data, server, status], get_status(Config)), @@ -168,6 +184,7 @@ remove_dead_from_cluster(Config) -> have_node_in_mnesia(Node2, Node3, false), % after node awakening nodes are clustered again distributed_helper:start_node(Node3Nodename, Config), + timer:sleep(1000), have_node_in_mnesia(Node1, Node3, true), have_node_in_mnesia(Node2, Node3, true). @@ -183,15 +200,20 @@ remove_alive_from_cluster(Config) -> %% Node2 is still running %% then get_ok_value([], remove_from_cluster(atom_to_binary(Node2Name), Config)), - % node is down hence its not in mnesia cluster have_node_in_mnesia(Node1, Node3, true), have_node_in_mnesia(Node1, Node2, false), have_node_in_mnesia(Node3, Node2, false). +remove_node_test(Config) -> + #{node := NodeName} = mim3(), + Value = get_ok_value([data, server, removeNode], remove_node(NodeName, Config)), + ?assertEqual(<<"Node deleted">>, Value). + stop_node_test(Config) -> #{node := Node1Name} = mim(), - #{node := Node3Nodename} = Node3 = mim3(), + #{node := Node3Nodename} = mim3(), get_ok_value([data, server, stop], stop_node(Node3Nodename, Config)), + timer:sleep(1000), Timeout = timer:seconds(3), {badrpc, nodedown} = rpc:call(Node3Nodename, mongoose_cluster, join, [Node1Name], Timeout), distributed_helper:start_node(Node3Nodename, Config). @@ -210,6 +232,9 @@ get_cookie(Config) -> get_loglevel(Config) -> execute_command(<<"server">>, <<"getLoglevel">>, #{}, Config). +set_loglevel(LogLevel, Config) -> + execute_command(<<"server">>, <<"setLoglevel">>, #{<<"level">> => LogLevel}, Config). + get_status(Config) -> execute_command(<<"server">>, <<"status">>, #{}, Config). @@ -225,5 +250,5 @@ remove_from_cluster(Node, Config) -> stop_node(Node, Config) -> execute_command(Node, <<"server">>, <<"stop">>, #{}, Config). -restart_node(Node, Config) -> - execute_command(Node, <<"server">>, <<"restart">>, #{}, Config). +remove_node(Node, Config) -> + execute_command(Node, <<"server">>, <<"removeNode">>, #{<<"node">> => Node}, Config). diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index e2ba16a441b..180ddcadf84 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -2,23 +2,42 @@ Allow admin to acquire data about the node """ type ServerAdminQuery @protected{ + "Get the status of the server" status: Status @protected(type: GLOBAL) - getLoglevel: String + "Get the loglevel of the server" + getLoglevel: LogLevel @protected(type: GLOBAL) + "Get the Erlang cookie of this node" getCookie: String @protected(type: GLOBAL) } +""" +Allow admin to manage the node +""" type ServerAdminMutation @protected{ + "Join the node to a cluster. Call it on the joining node" joinCluster(node: String!): String + @protected(type: GLOBAL) + "Leave a cluster. Call it on the node that is going to leave" leaveCluster: String - removeFromCluster(node: String): String + @protected(type: GLOBAL) + "Remove node from the cluster. Call it from the member of the cluster" + removeFromCluster(node: String!): String + @protected(type: GLOBAL) + "Restart mongooseim node" restart: String + @protected(type: GLOBAL) + "Stop mongooseim node" stop: String - - removeNode: String - setLoglevel: String + @protected(type: GLOBAL) + "Remove a MongooseIM node from Mnesia clustering config" + removeNode(node: String!): String + @protected(type: GLOBAL) + "Set Node's loglevel" + setLoglevel(level: LogLevel!): String + @protected(type: GLOBAL) } type Status { @@ -29,4 +48,17 @@ type Status { enum StatusCode { RUNNING NOT_RUNNING -} \ No newline at end of file +} + +enum LogLevel { + NONE + EMERGENCY + ALERT + CRITICAL + ERROR + WARNING + NOTICE + INFO + DEBUG + ALL +} diff --git a/src/admin_extra/service_admin_extra_node.erl b/src/admin_extra/service_admin_extra_node.erl index fb6d2cb516f..53162c65d0b 100644 --- a/src/admin_extra/service_admin_extra_node.erl +++ b/src/admin_extra/service_admin_extra_node.erl @@ -26,12 +26,9 @@ -module(service_admin_extra_node). -author('badlop@process-one.net'). --export([commands/0, - remove_node/1]). +-export([commands/0]). --ignore_xref([ - commands/0, load_config/1, get_cookie/0, remove_node/1 -]). +-ignore_xref([commands/0, load_config/1]). -include("ejabberd_commands.hrl"). @@ -44,24 +41,12 @@ commands() -> [ #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", - module = server_api, function = get_cookie, + module = mongoose_server_api, function = get_cookie, args = [], result = {cookie, string}}, #ejabberd_commands{name = remove_node, tags = [erlang], desc = "Remove a MongooseIM node from Mnesia clustering config", - module = ?MODULE, function = remove_node, + module = mongoose_server_api, function = remove_node, args = [{node, string}], result = {res, rescode}} ]. - - -%%% -%%% Node -%%% - - --spec remove_node(string()) -> 'ok'. -remove_node(Node) -> - mnesia:del_table_copy(schema, list_to_atom(Node)), - ok. - diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index b80258cbdbf..4efea10d2bb 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -69,7 +69,7 @@ commands() -> %% They are defined here so that other interfaces can use them too #ejabberd_commands{name = status, tags = [server], desc = "Get status of the ejabberd server", - module = server_api, function = status, + module = mongoose_server_api, function = status, args = [], result = {res, restuple}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", @@ -77,7 +77,7 @@ commands() -> args = [], result = {res, rescode}}, #ejabberd_commands{name = get_loglevel, tags = [logs, server], desc = "Get the current loglevel", - module = server_api, function = get_loglevel, + module = mongoose_server_api, function = get_loglevel, args = [], result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], @@ -156,19 +156,19 @@ commands() -> #ejabberd_commands{name = join_cluster, tags = [server], desc = "Join the node to a cluster. Call it from the joining node. Use `-f` or `--force` flag to avoid question prompt and force join the node", - module = server_api, function = join_cluster, + module = mongoose_server_api, function = join_cluster, args = [{node, string}], result = {res, restuple}}, #ejabberd_commands{name = leave_cluster, tags = [server], desc = "Leave a cluster. Call it from the node that is going to leave. Use `-f` or `--force` flag to avoid question prompt and force leave the node from cluster", - module = server_api, function = leave_cluster, + module = mongoose_server_api, function = leave_cluster, args = [], result = {res, restuple}}, #ejabberd_commands{name = remove_from_cluster, tags = [server], desc = "Remove dead node from the cluster. Call it from the member of the cluster. Use `-f` or `--force` flag to avoid question prompt and force remove the node", - module = server_api, function = remove_from_cluster, + module = mongoose_server_api, function = remove_from_cluster, args = [{node, string}], result = {res, restuple}} ]. diff --git a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl index 424e4622117..e679c31a043 100644 --- a/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl @@ -9,35 +9,40 @@ -include("../mongoose_graphql_types.hrl"). execute(_Ctx, server, <<"joinCluster">>, #{<<"node">> := Node}) -> - case server_api:join_cluster(binary_to_list(Node)) of + case mongoose_server_api:join_cluster(binary_to_list(Node)) of {mnesia_error, _} = Error -> make_error(Error, #{cluster => Node}); - {error, _} = Error -> - make_error(Error, #{cluster => Node}); + {error, Message} -> + make_error({internal_server_error, io_lib:format("~p", [Message])}, + #{cluster => Node}); {pang, String} -> make_error({timeout_error, String}, #{cluster => Node}); {_, String} -> {ok, String} end; execute(_Ctx, server, <<"removeFromCluster">>, #{<<"node">> := Node}) -> - case server_api:remove_from_cluster(binary_to_list(Node)) of + case mongoose_server_api:remove_from_cluster(binary_to_list(Node)) of {ok, _} = Result -> Result; Error -> make_error(Error, #{node => Node}) end; execute(_Ctx, server, <<"leaveCluster">>, #{}) -> - case server_api:leave_cluster() of - {error, _} = Error -> - make_error(Error, #{}); + case mongoose_server_api:leave_cluster() of + {error, Message} -> + make_error({internal_server_error, io_lib:format("~p", [Message])}, #{}); {not_in_cluster, String} -> make_error({not_in_cluster_error, String}, #{}); {_, String} -> {ok, String} end; +execute(_Ctx, server, <<"removeNode">>, #{<<"node">> := Node}) -> + mongoose_server_api:remove_node(binary_to_list(Node)); +execute(_Ctx, server, <<"setLoglevel">>, #{<<"level">> := LogLevel}) -> + mongoose_server_api:set_loglevel(LogLevel); execute(_Ctx, server, <<"stop">>, #{}) -> - spawn(server_api, stop, []), + spawn(mongoose_server_api, stop, []), {ok, "Stop scheduled"}; execute(_Ctx, server, <<"restart">>, #{}) -> - spawn(server_api, restart, []), + spawn(mongoose_server_api, restart, []), {ok, "Restart scheduled"}. diff --git a/src/graphql/admin/mongoose_graphql_server_admin_query.erl b/src/graphql/admin/mongoose_graphql_server_admin_query.erl index c0f46905a63..dc442b7e5a3 100644 --- a/src/graphql/admin/mongoose_graphql_server_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_server_admin_query.erl @@ -8,13 +8,13 @@ -include("../mongoose_graphql_types.hrl"). execute(_Ctx, server, <<"status">>, _) -> - case server_api:status() of + case mongoose_server_api:status() of {ok, String} -> {ok, #{<<"statusCode">> => <<"RUNNING">>, <<"message">> => String}}; {_, String} -> {ok, #{<<"statusCode">> => <<"NOT_RUNNING">>, <<"message">> => String}} end; execute(_Ctx, server, <<"getLoglevel">>, _) -> - server_api:get_loglevel(); + mongoose_server_api:graphql_get_loglevel(); execute(_Ctx, server, <<"getCookie">>, _) -> - {ok, server_api:get_cookie()}. + {ok, mongoose_server_api:get_cookie()}. diff --git a/src/graphql/mongoose_graphql_enum.erl b/src/graphql/mongoose_graphql_enum.erl index 0da9c442b3d..7903e24e1e7 100644 --- a/src/graphql/mongoose_graphql_enum.erl +++ b/src/graphql/mongoose_graphql_enum.erl @@ -33,6 +33,7 @@ input(<<"MUCAffiliation">>, <<"ADMIN">>) -> {ok, admin}; input(<<"MUCAffiliation">>, <<"OWNER">>) -> {ok, owner}; input(<<"PrivacyClassificationTags">>, Name) -> {ok, Name}; input(<<"TelephoneTags">>, Name) -> {ok, Name}; +input(<<"LogLevel">>, Name) -> {ok, list_to_atom(string:to_lower(binary_to_list(Name)))}; input(<<"MetricType">>, Name) -> {ok, Name}. output(<<"PresenceShow">>, Show) -> @@ -68,6 +69,7 @@ output(<<"MUCAffiliation">>, Aff) -> output(<<"AddressTags">>, Name) -> {ok, Name}; output(<<"EmailTags">>, Name) -> {ok, Name}; output(<<"PrivacyClassificationTags">>, Name) -> {ok, Name}; +output(<<"LogLevel">>, Name) -> {ok, list_to_binary(string:to_upper(atom_to_list(Name)))}; output(<<"TelephoneTags">>, Name) -> {ok, Name}; output(<<"MetricType">>, Type) -> {ok, Type}; output(<<"StatusCode">>, Code) -> {ok, Code}. diff --git a/src/graphql/mongoose_graphql_errors.erl b/src/graphql/mongoose_graphql_errors.erl index 4fa5ee4f4e9..d01b3d2c802 100644 --- a/src/graphql/mongoose_graphql_errors.erl +++ b/src/graphql/mongoose_graphql_errors.erl @@ -44,9 +44,6 @@ err(_Ctx, ErrorTerm) -> %% callback invoked when resolver crashes -spec crash(map(), term()) -> err_msg(). crash(_Ctx, Err = #{type := Type}) -> - io:format("---------------------------------------------------------------------------------------------------------\n"), - io:format("~p\n", [Err]), - io:format("---------------------------------------------------------------------------------------------------------\n"), ?LOG_ERROR(Err#{what => graphql_crash}), #{message => <<"Unexpected ", Type/binary, " resolver crash">>, extensions => #{code => resolver_crash}}. diff --git a/src/logger/mongoose_logs.erl b/src/logger/mongoose_logs.erl index 12955ae95b6..1660f14d87c 100644 --- a/src/logger/mongoose_logs.erl +++ b/src/logger/mongoose_logs.erl @@ -8,6 +8,8 @@ -export([dir/0]). -export([loglevel_keyword_to_number/1]). +-export_type([atom_log_level/0]). + -ignore_xref([clear_module_loglevel/1, set_module_loglevel/2]). -type atom_log_level() :: none | logger:level() | all. diff --git a/src/server_api.erl b/src/mongoose_server_api.erl similarity index 79% rename from src/server_api.erl rename to src/mongoose_server_api.erl index 8aed97ad092..5d60bd5df33 100644 --- a/src/server_api.erl +++ b/src/mongoose_server_api.erl @@ -1,15 +1,33 @@ --module(server_api). +-module(mongoose_server_api). -export([get_loglevel/0, status/0, get_cookie/0, join_cluster/1, leave_cluster/0, - remove_from_cluster/1, stop/0, restart/0]). + remove_from_cluster/1, stop/0, restart/0, remove_node/1, set_loglevel/1, + graphql_get_loglevel/0]). --spec get_loglevel() -> {ok, string()}. +-ignore_xref([get_loglevel/0]). + +-spec get_loglevel() -> {ok, iodata()}. get_loglevel() -> Level = mongoose_logs:get_global_loglevel(), Number = mongoose_logs:loglevel_keyword_to_number(Level), String = io_lib:format("global loglevel is ~p, which means '~p'", [Number, Level]), {ok, String}. +-spec graphql_get_loglevel() -> {ok, mongoose_logs:atom_log_level()}. +graphql_get_loglevel() -> + {ok, mongoose_logs:get_global_loglevel()}. + + +-spec set_loglevel(mongoose_logs:atom_log_level()) -> + {ok, iodata()} | {invalid_level, iodata()}. +set_loglevel(Level) -> + case mongoose_logs:set_global_loglevel(Level) of + ok -> + {ok, "Log level successfully set"}; + {error, _} -> + {invalid_level, io_lib:format("Log level ~p does not exist", [Level])} + end. + -spec status() -> {'mongooseim_not_running', io_lib:chars()} | {'ok', io_lib:chars()}. status() -> {InternalStatus, ProvidedStatus} = init:get_status(), @@ -27,7 +45,7 @@ get_cookie() -> atom_to_list(erlang:get_cookie()). -spec join_cluster(string()) -> {ok, string()} | {pang, string()} | {already_joined, string()} | - {mnesia_error, string()} | {error, string()}. + {mnesia_error, string()} | {error, any()}. join_cluster(NodeString) -> NodeAtom = list_to_atom(NodeString), NodeList = mnesia:system_info(db_nodes), @@ -96,7 +114,8 @@ remove_from_cluster(NodeString) -> remove_dead_node(DeadNode) -> try mongoose_cluster:remove_from_cluster(DeadNode) of ok -> - String = io_lib:format("The dead node ~p has been removed from the cluster~n", [DeadNode]), + String = + io_lib:format("The dead node ~p has been removed from the cluster~n", [DeadNode]), {ok, String} catch error:{node_is_alive, DeadNode} -> @@ -110,7 +129,8 @@ remove_dead_node(DeadNode) -> remove_rpc_alive_node(AliveNode) -> case rpc:call(AliveNode, mongoose_cluster, leave, []) of {badrpc, Reason} -> - String = io_lib:format("Cannot remove the node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), + String = + io_lib:format("Cannot remove the node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), {rpc_error, String}; ok -> String = io_lib:format("The node ~p has been removed from the cluster~n", [AliveNode]), @@ -120,10 +140,17 @@ remove_rpc_alive_node(AliveNode) -> {rpc_error, String} end. +-spec stop() -> ok. stop() -> timer:sleep(500), init:stop(). +-spec restart() -> ok. restart() -> timer:sleep(500), init:restart(). + +-spec remove_node(string()) -> {ok, string()}. +remove_node(Node) -> + mnesia:del_table_copy(schema, list_to_atom(Node)), + {ok, "Node deleted"}. diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1757ddf835dc026210e9c5f2c597cffb8061c937 GIT binary patch literal 14340 zcmeHNO>Y}T7@kej;2eVAlw8`Ir-Dlc5Asut$kR3EFm>YlojPNROpomYo6N}a~{ z?3GC%lRzecOahq%{*Mx1?!_yY@$-~C! zUN}dIF4v05xSouMdK6O72q{KQ8YEkcK$GKok+)-9PsT$%YH}55aup;q2gwd4kl8_3 zl-pIXuE*>%lRzecaS6PQOH0^yN2-QhbXoPWbLZ|ol+m@TejhvWAB|o0g(1>5cIA<( z0Fk@%8us>G^aVN?D}KB63AhcQ#OMr^6#E05qb!_v)tVbOxH!V^@{r#h;IMnKgN{Rt zb7w~(_+|v_Xzh{O!n=jFhCm}G$1cA6;Qld!TNv`#2TBDv5qch93ZK-KLr2mrF?DoN z@Iti>oCs*-unbu?F;;P9C;8optf2krONRE(r699AQwM%1`f_8UPh(-8O~s6Ww*`*3 z-Rq~GEW~s!(-CR8W!i(?fubG#eauR~+fKS#KUdnVW;b44{WX|6HhuiWYq{L1+^LyU zpKmsz{$^Z`d)(SBm@ZUdQ>3wVMYACvz)rU3>5D zgPpy{`(GVA`A$cWAj!gNZX}|7kMA!e;N8tm*os25_K@cH5gJGA*2p7rBa$O zRz@;p8T}nl#>6d{TCXT?FoCZtxvar)QZvpqQ+5=h#JokimI-nC>Tk(YU5lqZtiex)jeFg6()?Gq^i68}rua0~D;=Crl4u)!PD0%*!b<=tAoi@cBYECRXsLVFhVY=)*S{&^?>s z(>}o3o$Q^;{SnXD|H77U&qVsqnJ&;_%G_Ld9`mYKSg^4QKWU0F!yfHdgfvB0F} zV%US;%I>Ngun(8KAax6KUJ5*I&Gb1p0_?y-SbJXN z<8?ZO7?`P?g7D@>n-m!75++_=@cQClK4G86`MY791<2Uq1-ADfwP=oYzH_TzE zA}tf|tT*0-6^iMbh|4L6RdwCRy(NH{SQoson8bCXy*r`er^}hRiz&FyR4%q*qNM4f z0Zy0}Jc1bm&(s%&ZqKK1#eIq1<I(>}AhlR9| z?ABOLY3aE!;8zyL@(^S22bu0cd+33BwXo*LupC|`n*7Dk8l~~Sr$s~|T;3Z2Sn7+Mvdjp`s1YkaESH$rE$KZu~<#v3?qj(tQQ9OL% zqj>KO?&FBx)|2s2k3x#;LW=PpdI&JJ!yqO2tdY*M{r|t@Nt0~<|CH{8M`qdUc)tDr EUmXSCE&u=k literal 0 HcmV?d00001 diff --git a/test/mongoose_config_SUITE.erl b/test/mongoose_config_SUITE.erl index a19d79c3b42..87d48955685 100644 --- a/test/mongoose_config_SUITE.erl +++ b/test/mongoose_config_SUITE.erl @@ -244,7 +244,7 @@ code_paths() -> [filename:absname(Path) || Path <- code:get_path()]. maybe_join_cluster(SlaveNode) -> - Result = rpc:call(SlaveNode, server_api, join_cluster, + Result = rpc:call(SlaveNode, mongoose_server_api, join_cluster, [atom_to_list(node())]), case Result of {ok, _} -> From 49f4a28df3003833129c487a53d8a94bd8fc68fc Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Tue, 13 Sep 2022 14:09:14 +0200 Subject: [PATCH 033/117] Fixing CR comments --- big_tests/tests/graphql_server_SUITE.erl | 49 ++++++++++++------------ src/mongoose_server_api.erl | 9 ++--- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl index d75752116de..253b61549fe 100644 --- a/big_tests/tests/graphql_server_SUITE.erl +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -16,7 +16,7 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [%{group, admin_http}, + [%{group, admin_http}, % http is not supported for the server category {group, admin_cli}]. groups() -> @@ -47,19 +47,12 @@ clustering_tests() -> init_per_suite(Config) -> Config1 = dynamic_modules:save_modules(host_type(), Config), - #{node := Node1} = RPCNode1 = mim(), - #{node := Node2} = RPCNode2 = mim2(), - #{node := Node3} = RPCNode3 = mim3(), - Config2 = ejabberd_node_utils:init(RPCNode1, Config1), - Config3 = ejabberd_node_utils:init(RPCNode2, Config2), - Config4 = ejabberd_node_utils:init(RPCNode3, Config3), - NodeCtlPath = distributed_helper:ctl_path(Node1, Config4), - Node2CtlPath = distributed_helper:ctl_path(Node2, Config4), - Node3CtlPath = distributed_helper:ctl_path(Node3, Config4), - escalus:init_per_suite([{ctl_path_atom(Node1), NodeCtlPath}, - {ctl_path_atom(Node2), Node2CtlPath}, - {ctl_path_atom(Node3), Node3CtlPath}] - ++ Config4). + Config2 = lists:foldl(fun(#{node := Node} = RPCNode, ConfigAcc) -> + ConfigAcc1 = ejabberd_node_utils:init(RPCNode, ConfigAcc), + NodeCtlPath = distributed_helper:ctl_path(Node, ConfigAcc1), + ConfigAcc1 ++ [{ctl_path_atom(Node), NodeCtlPath}] + end, Config1, [mim(), mim2(), mim3()]), + escalus:init_per_suite(Config2). ctl_path_atom(NodeName) -> CtlString = atom_to_list(NodeName) ++ "_ctl", @@ -114,16 +107,7 @@ get_cookie_test(Config) -> ?assert(is_binary(Result)). set_and_get_loglevel_test(Config) -> - LogLevels = [<<"NONE">>, - <<"EMERGENCY">>, - <<"ALERT">>, - <<"CRITICAL">>, - <<"ERROR">>, - <<"WARNING">>, - <<"NOTICE">>, - <<"INFO">>, - <<"DEBUG">>, - <<"ALL">>], + LogLevels = all_log_levels(), lists:foreach(fun(LogLevel) -> Value = get_ok_value([data, server, setLoglevel], set_loglevel(LogLevel, Config)), ?assertEqual(<<"Log level successfully set">>, Value), @@ -207,7 +191,7 @@ remove_alive_from_cluster(Config) -> remove_node_test(Config) -> #{node := NodeName} = mim3(), Value = get_ok_value([data, server, removeNode], remove_node(NodeName, Config)), - ?assertEqual(<<"Node deleted">>, Value). + ?assertEqual(<<"Node removed from the Mnesia schema">>, Value). stop_node_test(Config) -> #{node := Node1Name} = mim(), @@ -222,6 +206,18 @@ stop_node_test(Config) -> % Helpers %----------------------------------------------------------------------- +all_log_levels() -> + [<<"NONE">>, + <<"EMERGENCY">>, + <<"ALERT">>, + <<"CRITICAL">>, + <<"ERROR">>, + <<"WARNING">>, + <<"NOTICE">>, + <<"INFO">>, + <<"DEBUG">>, + <<"ALL">>]. + have_node_in_mnesia(Node1, #{node := Node2}, ShouldBe) -> DbNodes1 = distributed_helper:rpc(Node1, mnesia, system_info, [db_nodes]), ?assertEqual(ShouldBe, lists:member(Node2, DbNodes1)). @@ -238,6 +234,9 @@ set_loglevel(LogLevel, Config) -> get_status(Config) -> execute_command(<<"server">>, <<"status">>, #{}, Config). +get_status(Node, Config) -> + execute_command(Node, <<"server">>, <<"status">>, #{}, Config). + join_cluster(Node, Config) -> execute_command(<<"server">>, <<"joinCluster">>, #{<<"node">> => Node}, Config). diff --git a/src/mongoose_server_api.erl b/src/mongoose_server_api.erl index 5d60bd5df33..d2c5d900fde 100644 --- a/src/mongoose_server_api.erl +++ b/src/mongoose_server_api.erl @@ -6,7 +6,7 @@ -ignore_xref([get_loglevel/0]). --spec get_loglevel() -> {ok, iodata()}. +-spec get_loglevel() -> {ok, string()}. get_loglevel() -> Level = mongoose_logs:get_global_loglevel(), Number = mongoose_logs:loglevel_keyword_to_number(Level), @@ -17,9 +17,8 @@ get_loglevel() -> graphql_get_loglevel() -> {ok, mongoose_logs:get_global_loglevel()}. - -spec set_loglevel(mongoose_logs:atom_log_level()) -> - {ok, iodata()} | {invalid_level, iodata()}. + {ok, string()} | {invalid_level, string()}. set_loglevel(Level) -> case mongoose_logs:set_global_loglevel(Level) of ok -> @@ -28,7 +27,7 @@ set_loglevel(Level) -> {invalid_level, io_lib:format("Log level ~p does not exist", [Level])} end. --spec status() -> {'mongooseim_not_running', io_lib:chars()} | {'ok', io_lib:chars()}. +-spec status() -> {'mongooseim_not_running', string()} | {'ok', string()}. status() -> {InternalStatus, ProvidedStatus} = init:get_status(), String1 = io_lib:format("The node ~p is ~p. Status: ~p", @@ -153,4 +152,4 @@ restart() -> -spec remove_node(string()) -> {ok, string()}. remove_node(Node) -> mnesia:del_table_copy(schema, list_to_atom(Node)), - {ok, "Node deleted"}. + {ok, "Node removed from the Mnesia schema"}. From 024fe418d46f9ae64eeaf20137224c544cae056a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Thu, 15 Sep 2022 11:05:26 +0200 Subject: [PATCH 034/117] Changed node_cleanup hook handler in ejabberd_router module to gen_hook format --- src/ejabberd_router.erl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 0c73243322f..1cd0a92d44d 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -49,7 +49,7 @@ ]). -export([start_link/0]). --export([routes_cleanup_on_nodedown/2]). +-export([routes_cleanup_on_nodedown/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -58,8 +58,7 @@ -export([update_tables/0]). -ignore_xref([register_component/2, register_component/3, register_component/4, - register_components/2, register_components/3, - route_error/4, routes_cleanup_on_nodedown/2, start_link/0, + register_components/2, register_components/3, route_error/4, start_link/0, unregister_component/1, unregister_component/2, unregister_components/2, unregister_routes/1, update_tables/0]). @@ -346,7 +345,7 @@ init([]) -> {record_name, external_component}]), mnesia:add_table_copy(external_component_global, node(), ram_copies), mongoose_metrics:ensure_metric(global, routingErrors, spiral), - ejabberd_hooks:add(node_cleanup, global, ?MODULE, routes_cleanup_on_nodedown, 90), + gen_hook:add_handlers(hooks()), {ok, #state{}}. @@ -361,7 +360,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> - ejabberd_hooks:delete(node_cleanup, global, ?MODULE, routes_cleanup_on_nodedown, 90), + gen_hook:delete_handlers(hooks()), ok. code_change(_OldVsn, State, _Extra) -> @@ -370,6 +369,10 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- +-spec hooks() -> [gen_hook:hook_tuple()]. +hooks() -> + [{node_cleanup, global, fun ?MODULE:routes_cleanup_on_nodedown/3, #{}, 90}]. + routing_modules_list() -> mongoose_config:get_opt(routing_modules). @@ -428,9 +431,9 @@ update_tables() -> ok end. --spec routes_cleanup_on_nodedown(map(), node()) -> map(). -routes_cleanup_on_nodedown(Acc, Node) -> +-spec routes_cleanup_on_nodedown(map(), map(), map()) -> {ok, map()}. +routes_cleanup_on_nodedown(Acc, #{node := Node}, _) -> Entries = mnesia:dirty_match_object(external_component_global, #external_component{node = Node, _ = '_'}), [mnesia:dirty_delete_object(external_component_global, Entry) || Entry <- Entries], - maps:put(?MODULE, ok, Acc). + {ok, maps:put(?MODULE, ok, Acc)}. From a1ff38ef53c8da663a1e4119c313a4b073ee157e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:21:21 +0200 Subject: [PATCH 035/117] Test mongoose_admin_api instead of the obsolete mongoose_api_admin Also: remove mod_*commands from small tests, because this functionality is being removed. --- test/common/config_parser_helper.erl | 7 +++---- test/config_parser_SUITE.erl | 10 +++++----- test/config_parser_SUITE_data/mongooseim-pgsql.toml | 10 ++-------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 1c47c933664..ef9c7ad92a8 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -170,7 +170,7 @@ options("mongooseim-pgsql") -> config([listen, http, handlers, mod_websockets], #{host => '_', path => "/ws-xmpp", max_stanza_size => 100, ping_rate => 120000, timeout => infinity}), - config([listen, http, handlers, mongoose_api_admin], + config([listen, http, handlers, mongoose_admin_api], #{host => "localhost", path => "/api", username => <<"ala">>, password => <<"makotaipsa">>}) ], @@ -183,7 +183,7 @@ options("mongooseim-pgsql") -> port => 8088, transport => #{num_acceptors => 10, max_connections => 1024}, handlers => - [config([listen, http, handlers, mongoose_api_admin], + [config([listen, http, handlers, mongoose_admin_api], #{host => "localhost", path => "/api"})] }), config([listen, http], @@ -680,10 +680,9 @@ pgsql_modules() -> #{mod_adhoc => default_mod_config(mod_adhoc), mod_amp => #{}, mod_blocking => default_mod_config(mod_blocking), mod_bosh => default_mod_config(mod_bosh), - mod_carboncopy => default_mod_config(mod_carboncopy), mod_commands => #{}, + mod_carboncopy => default_mod_config(mod_carboncopy), mod_disco => mod_config(mod_disco, #{users_can_see_hidden_services => false}), mod_last => mod_config(mod_last, #{backend => rdbms}), - mod_muc_commands => #{}, mod_muc_light_commands => #{}, mod_offline => mod_config(mod_offline, #{backend => rdbms}), mod_privacy => mod_config(mod_privacy, #{backend => rdbms}), mod_private => default_mod_config(mod_private), diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index e97d7e0b7ee..d65ca2832d8 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -97,8 +97,8 @@ groups() -> listen_http_handlers_bosh, listen_http_handlers_websockets, listen_http_handlers_client_api, + listen_http_handlers_admin_api, listen_http_handlers_api, - listen_http_handlers_api_admin, listen_http_handlers_domain, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, @@ -603,16 +603,16 @@ listen_http_handlers_client_api(_Config) -> ?err(T(#{<<"handlers">> => [not_a_module]})), ?err(T(#{<<"docs">> => <<"maybe">>})). +listen_http_handlers_admin_api(_Config) -> + {P, T} = test_listen_http_handler(mongoose_admin_api), + test_listen_http_handler_creds(P, T). + listen_http_handlers_api(_Config) -> {P, T} = test_listen_http_handler(mongoose_api), ?cfg(P ++ [handlers], [mongoose_api_metrics], T(#{<<"handlers">> => [<<"mongoose_api_metrics">>]})), ?err(T(#{<<"handlers">> => [not_a_module]})). -listen_http_handlers_api_admin(_Config) -> - {P, T} = test_listen_http_handler(mongoose_api_admin), - test_listen_http_handler_creds(P, T). - listen_http_handlers_domain(_Config) -> {P, T} = test_listen_http_handler(mongoose_domain_handler), test_listen_http_handler_creds(P, T). diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index 301b3d01c2d..3f4f4e8e475 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -38,7 +38,7 @@ tls.keyfile = "priv/dc1.pem" tls.password = "" - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" username = "ala" @@ -61,7 +61,7 @@ transport.num_acceptors = 10 transport.max_connections = 1024 - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" @@ -176,12 +176,6 @@ [modules.mod_disco] users_can_see_hidden_services = false -[modules.mod_commands] - -[modules.mod_muc_commands] - -[modules.mod_muc_light_commands] - [modules.mod_last] backend = "rdbms" From 818bbf195f71ceab7792c2506c74b7b5a2959b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:24:08 +0200 Subject: [PATCH 036/117] Finish replacing mongoose_api_admin with mongoose_admin_api --- src/mongoose_http_handler.erl | 3 +-- src/system_metrics/mongoose_system_metrics_collector.erl | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index efc7cf011fb..eee7434a57a 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -83,8 +83,7 @@ cowboy_host(Host) -> Host. configurable_handler_modules() -> [mod_websockets, mongoose_client_api, - mongoose_api, - mongoose_api_admin, mongoose_admin_api, + mongoose_api, mongoose_domain_handler, mongoose_graphql_cowboy_handler]. diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 7d7f5ce5589..2fcca06f7b7 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -126,7 +126,7 @@ get_api() -> [#{report_name => http_api, key => Api, value => enabled} || Api <- ApiList]. filter_unknown_api(ApiList) -> - AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_api_admin, + AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_admin_api, mongoose_domain_handler, mod_bosh, mod_websockets], [Api || Api <- ApiList, lists:member(Api, AllowedToReport)]. From 3d7e00b7b4784392a948507c21583bc0eda46b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:27:24 +0200 Subject: [PATCH 037/117] Remove the obsolete mongoose_api_admin --- src/mongoose_api_admin.erl | 218 ------------------------------------- 1 file changed, 218 deletions(-) delete mode 100644 src/mongoose_api_admin.erl diff --git a/src/mongoose_api_admin.erl b/src/mongoose_api_admin.erl deleted file mode 100644 index 0419a3d8bf3..00000000000 --- a/src/mongoose_api_admin.erl +++ /dev/null @@ -1,218 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author ludwikbukowski -%%% @copyright (C) 2016, Erlang Solutions Ltd. -%%% Created : 05. Jul 2016 12:59 -%%%------------------------------------------------------------------- - -%% @doc MongooseIM REST HTTP API for administration. -%% This module implements cowboy REST callbacks and -%% passes the requests on to the http api backend module. -%% @end --module(mongoose_api_admin). --author("ludwikbukowski"). - --behaviour(mongoose_http_handler). --behaviour(cowboy_rest). - -%% mongoose_http_handler callbacks --export([config_spec/0, routes/1]). - -%% config processing callbacks --export([process_config/1]). - -%% cowboy_rest exports --export([allowed_methods/2, - content_types_provided/2, - terminate/3, - init/2, - options/2, - content_types_accepted/2, - delete_resource/2, - is_authorized/2]). -%% local callbacks --export([to_json/2, from_json/2]). - --ignore_xref([cowboy_router_paths/2, from_json/2, to_json/2]). - --include("mongoose_api.hrl"). --include("mongoose.hrl"). --include("mongoose_config_spec.hrl"). - --import(mongoose_api_common, [error_response/4, - action_to_method/1, - method_to_action/1, - error_code/1, - process_request/4, - parse_request_body/1]). - --type credentials() :: {Username :: binary(), Password :: binary()} | any. - --type handler_options() :: #{path := string(), username => binary(), password => binary(), - atom() => any()}. - -%%-------------------------------------------------------------------- -%% mongoose_http_handler callbacks -%%-------------------------------------------------------------------- - --spec config_spec() -> mongoose_config_spec:config_section(). -config_spec() -> - #section{items = #{<<"username">> => #option{type = binary}, - <<"password">> => #option{type = binary}}, - process = fun ?MODULE:process_config/1}. - --spec process_config(handler_options()) -> handler_options(). -process_config(Opts) -> - case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of - true -> - Opts; - false -> - error(#{what => both_username_and_password_required, opts => Opts}) - end. - --spec routes(handler_options()) -> mongoose_http_handler:routes(). -routes(Opts = #{path := BasePath}) -> - ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), - ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), - try - Commands = mongoose_commands:list(admin), - [handler_path(BasePath, Command, Opts) || Command <- Commands] - catch - Class:Err:StackTrace -> - ?LOG_ERROR(#{what => getting_command_list_error, - class => Class, reason => Err, stacktrace => StackTrace}), - [] - end. - -%%-------------------------------------------------------------------- -%% cowboy_rest callbacks -%%-------------------------------------------------------------------- - -init(Req, Opts) -> - Bindings = maps:to_list(cowboy_req:bindings(Req)), - #{command_category := CommandCategory, command_subcategory := CommandSubCategory} = Opts, - Auth = auth_opts(Opts), - State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(admin), - bindings = Bindings, - command_category = CommandCategory, - command_subcategory = CommandSubCategory, - auth = Auth}, - {cowboy_rest, Req, State}. - -auth_opts(#{username := UserName, password := Password}) -> {UserName, Password}; -auth_opts(#{}) -> any. - -options(Req, State) -> - Req1 = set_cors_headers(Req), - {ok, Req1, State}. - -set_cors_headers(Req) -> - Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>, - <<"GET, OPTIONS, PUT, POST, DELETE">>, Req), - Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, - <<"*">>, Req1), - cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>, - <<"Content-Type">>, Req2). - -allowed_methods(Req, #http_api_state{command_category = Name} = State) -> - CommandList = mongoose_commands:list(admin, Name), - AllowedMethods = [action_to_method(mongoose_commands:action(Command)) - || Command <- CommandList], - {[<<"OPTIONS">> | AllowedMethods], Req, State}. - -content_types_provided(Req, State) -> - CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], - {CTP, Req, State}. - -content_types_accepted(Req, State) -> - CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], - {CTA, Req, State}. - -terminate(_Reason, _Req, _State) -> - ok. - -%% @doc Called for a method of type "DELETE" -delete_resource(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Arity = length(B), - Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"DELETE">>), SubCategory), - [Command] = [C || C <- Cmds, mongoose_commands:arity(C) == Arity], - process_request(<<"DELETE">>, Command, Req, State). - - -%%-------------------------------------------------------------------- -%% Authorization -%%-------------------------------------------------------------------- - -% @doc Cowboy callback -is_authorized(Req, State) -> - ControlCreds = get_control_creds(State), - AuthDetails = mongoose_api_common:get_auth_details(Req), - case authorize(ControlCreds, AuthDetails) of - true -> - {true, Req, State}; - false -> - mongoose_api_common:make_unauthorized_response(Req, State) - end. - --spec authorize(credentials(), {AuthMethod :: atom(), - Username :: binary(), - Password :: binary()}) -> boolean(). -authorize(any, _) -> true; -authorize(_, undefined) -> false; -authorize(ControlCreds, {AuthMethod, User, Password}) -> - compare_creds(ControlCreds, {User, Password}) andalso - mongoose_api_common:is_known_auth_method(AuthMethod). - -% @doc Checks if credentials are the same (if control creds are 'any' -% it is equal to everything). --spec compare_creds(credentials(), credentials() | undefined) -> boolean(). -compare_creds({User, Pass}, {User, Pass}) -> true; -compare_creds(_, _) -> false. - -get_control_creds(#http_api_state{auth = Creds}) -> - Creds. - -%%-------------------------------------------------------------------- -%% Internal funs -%%-------------------------------------------------------------------- - -%% @doc Called for a method of type "GET" -to_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"GET">>), SubCategory), - Arity = length(B), - case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of - [Command] -> - process_request(<<"GET">>, Command, Req, State); - [] -> - error_response(not_found, ?ARGS_LEN_ERROR, Req, State) - end. - -%% @doc Called for a method of type "POST" and "PUT" -from_json(Req, #http_api_state{command_category = Category, - command_subcategory = SubCategory, - bindings = B} = State) -> - case parse_request_body(Req) of - {error, _R}-> - error_response(bad_request, ?BODY_MALFORMED, Req, State); - {Params, _} -> - Method = cowboy_req:method(Req), - Cmds = mongoose_commands:list(admin, Category, method_to_action(Method), SubCategory), - QVals = cowboy_req:parse_qs(Req), - Arity = length(B) + length(Params) + length(QVals), - case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of - [Command] -> - process_request(Method, Command, {Params, Req}, State); - [] -> - error_response(not_found, ?ARGS_LEN_ERROR, Req, State) - end - end. - --spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t(), handler_options()) -> - ejabberd_cowboy:route(). -handler_path(Base, Command, CommonOpts) -> - {[Base, mongoose_api_common:create_admin_url_path(Command)], - ?MODULE, CommonOpts#{command_category => mongoose_commands:category(Command), - command_subcategory => mongoose_commands:subcategory(Command)}}. From 898e16289cc6e13853c9a7df9f01a058e1a03940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:20:10 +0200 Subject: [PATCH 038/117] Remove tests for mongoose_commands This functionality is being removed. --- test/commands_SUITE.erl | 627 ----------------------------- test/commands_backend_SUITE.erl | 536 ------------------------ test/mongoose_api_common_SUITE.erl | 72 ---- 3 files changed, 1235 deletions(-) delete mode 100644 test/commands_SUITE.erl delete mode 100644 test/commands_backend_SUITE.erl delete mode 100644 test/mongoose_api_common_SUITE.erl diff --git a/test/commands_SUITE.erl b/test/commands_SUITE.erl deleted file mode 100644 index 68f49b7d3e9..00000000000 --- a/test/commands_SUITE.erl +++ /dev/null @@ -1,627 +0,0 @@ -%% @doc This suite tests both old ejabberd_commands module, which is slowly getting deprecated, -%% and the new mongoose_commands implementation. --module(commands_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("exml/include/exml.hrl"). --include_lib("eunit/include/eunit.hrl"). --include("ejabberd_commands.hrl"). --include("jlib.hrl"). - --define(PRT(X, Y), ct:pal("~p: ~p", [X, Y])). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% suite configuration -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -all() -> - [ - {group, old_commands}, - {group, new_commands} - ]. - -groups() -> - [ - {old_commands, [sequence], - [old_list, - old_exec, - old_access_ctl - ] - }, - {new_commands, [sequence], - [new_type_checker, - new_reg_unreg, - new_failedreg, - new_list, - new_execute, - different_types, - errors_are_readable - ] - } - ]. - -init_per_suite(C) -> - application:ensure_all_started(jid), - ok = mnesia:start(), - C. - -end_per_suite(_) -> - mnesia:stop(), - mnesia:delete_schema([node()]), - ok. - -init_per_group(old_commands, C) -> - Pid = spawn(fun ec_holder/0), - [{helper_proc, Pid} | C]; -init_per_group(new_commands, C) -> - Pid = spawn(fun mc_holder/0), - [{helper_proc, Pid} | C]. - -end_per_group(old_commands, C) -> - ejabberd_commands:unregister_commands(commands_old()), - stop_helper_proc(C), - C; -end_per_group(new_commands, C) -> - mongoose_commands:unregister(commands_new()), - stop_helper_proc(C), - C. - -stop_helper_proc(C) -> - Pid = proplists:get_value(helper_proc, C), - Pid ! stop. - -init_per_testcase(_, C) -> - [mongoose_config:set_opt(Key, Value) || {Key, Value} <- opts()], - meck:new(ejabberd_auth_dummy, [non_strict]), - meck:expect(ejabberd_auth_dummy, get_password_s, fun(_, _) -> <<"">> end), - meck:new(mongoose_domain_api), - meck:expect(mongoose_domain_api, get_domain_host_type, fun(H) -> {ok, H} end), - C. - -end_per_testcase(_, _C) -> - [mongoose_config:unset_opt(Key) || {Key, _Value} <- opts()], - meck:unload(). - -opts() -> - [{{auth, <<"localhost">>}, #{methods => [dummy]}}, - {{access, <<"localhost">>}, #{experts_only => [#{acl => coder, value => allow}, - #{acl => manager, value => allow}, - #{acl => all, value => deny}]}}, - {{acl, <<"localhost">>}, #{coder => [#{user => <<"zenek">>, match => current_domain}]}}]. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% test methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - -old_list(_C) -> - %% list - Rlist = ejabberd_commands:list_commands(), - {command_one, _, "do nothing and return"} = proplists:lookup(command_one, Rlist), - %% get definition - Rget = ejabberd_commands:get_command_definition(command_one), - % we should get back exactly the definition we provided - [Cone | _] = commands_old(), - Cone = Rget, - %% get interface - {Argspec, Retspec} = ejabberd_commands:get_command_format(command_one), - [{msg, binary}] = Argspec, - {res, restuple} = Retspec, - %% list by tags - Tagcomm = ejabberd_commands:get_tags_commands(), - ?assertEqual(length(proplists:get_value("one", Tagcomm)), 1), - ?assertEqual(length(proplists:get_value("two", Tagcomm)), 2), - ?assertEqual(length(proplists:get_value("three", Tagcomm)), 1), - ok. - -old_exec(_C) -> - %% execute - <<"bzzzz">> = ejabberd_commands:execute_command(command_one, [<<"bzzzz">>]), - Res = ejabberd_commands:execute_command(command_one, [123]), - ?PRT("invalid type ignored", Res), %% there is no arg type check - Res2 = ejabberd_commands:execute_command(command_two, [123]), - ?PRT("invalid return type ignored", Res2), %% nor return - %% execute unknown command - {error, command_unknown} = ejabberd_commands:execute_command(command_seven, [123]), - ok. - -old_access_ctl(_C) -> - %% with no auth method it is all fine - checkauth(true, #{}, noauth), - %% noauth fails if first item is not 'all' (users) - checkauth(account_unprivileged, #{none => command_rules(all)}, noauth), - %% if here we allow all commands to noauth - checkauth(true, #{all => command_rules(all)}, noauth), - %% and here only command_one - checkauth(true, #{all => command_rules([command_one])}, noauth), - %% so this'd fail - checkauth(account_unprivileged, #{all => command_rules([command_two])}, noauth), - % now we provide a role name, this requires a user and triggers password and acl check - % this fails because password is bad - checkauth(invalid_account_data, #{some_acl_role => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"bbb">>}), - % this, because of acl - checkauth(account_unprivileged, #{some_acl_role => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"">>}), - % and this should work, because we define command_one as available to experts only, while acls in config - % (see ggo/1) state that experts-only funcs are available to coders and managers, and zenek is a coder, gah. - checkauth(true, #{experts_only => command_rules([command_one])}, - {<<"zenek">>, <<"localhost">>, <<"">>}), - ok. - -new_type_checker(_C) -> - true = t_check_type({msg, binary}, <<"zzz">>), - true = t_check_type({msg, integer}, 127), - {false, _} = t_check_type({{a, binary}, {b, integer}}, 127), - true = t_check_type({{a, binary}, {b, integer}}, {<<"z">>, 127}), - true = t_check_type({ok, {msg, integer}}, {ok, 127}), - true = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), - {false, _} = t_check_type({k, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), - {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, "z"}), - {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>, 333}), - true = t_check_type([integer], []), - true = t_check_type([integer], [1, 2, 3]), - {false, _} = t_check_type([integer], [1, <<"z">>, 3]), - true = t_check_type([], [1, 2, 3]), - true = t_check_type([], []), - true = t_check_type({msg, boolean}, true), - true = t_check_type({msg, boolean}, false), - {false, _} = t_check_type({msg, boolean}, <<"true">>), - ok. - -t_check_type(Spec, Value) -> - R = try mongoose_commands:check_type(argument, Spec, Value) of - true -> true - catch - E -> - {false, E} - end, - R. - -new_reg_unreg(_C) -> - L1 = length(commands_new()), - L2 = L1 + length(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L1), - mongoose_commands:register(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L2), - mongoose_commands:unregister(commands_new_temp()), - ?assertEqual(length(mongoose_commands:list(admin)), L1), - ok. - -failedreg([]) -> ok; -failedreg([Cmd|Tail]) -> - ?assertThrow({invalid_command_definition, _}, mongoose_commands:register([Cmd])), - failedreg(Tail). - -new_failedreg(_C) -> - failedreg(commands_new_lame()). - - -new_list(_C) -> - %% for admin - Rlist = mongoose_commands:list(admin), - [Cmd] = [C || C <- Rlist, mongoose_commands:name(C) == command_one], - command_one = mongoose_commands:name(Cmd), - <<"do nothing and return">> = mongoose_commands:desc(Cmd), - %% list by category - [_] = mongoose_commands:list(admin, <<"user">>), - [] = mongoose_commands:list(admin, <<"nocategory">>), - %% list by category and action - [_] = mongoose_commands:list(admin, <<"user">>, read), - [] = mongoose_commands:list(admin, <<"user">>, update), - %% get definition - Rget = mongoose_commands:get_command(admin, command_one), - command_one = mongoose_commands:name(Rget), - read = mongoose_commands:action(Rget), - [] = mongoose_commands:identifiers(Rget), - {error, denied, _} = mongoose_commands:get_command(ujid(), command_one), - %% list for a user - Ulist = mongoose_commands:list(ujid()), - [UCmd] = [UC || UC <- Ulist, mongoose_commands:name(UC) == command_foruser], - - command_foruser = mongoose_commands:name(UCmd), - URget = mongoose_commands:get_command(ujid(), command_foruser), - command_foruser = mongoose_commands:name(URget), - ok. - - -new_execute(_C) -> - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, [<<"bzzzz">>]), - Cmd = mongoose_commands:get_command(admin, command_one), - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, Cmd, [<<"bzzzz">>]), - %% call with a map - {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>}), - %% command which returns just ok - ok = mongoose_commands:execute(admin, command_noreturn, [<<"bzzzz">>]), - %% this user has no permissions - {error, denied, _} = mongoose_commands:execute(ujid(), command_one, [<<"bzzzz">>]), - %% command is not registered - {error, not_implemented, _} = mongoose_commands:execute(admin, command_seven, [<<"bzzzz">>]), - %% invalid arguments - {error, type_error, _} = mongoose_commands:execute(admin, command_one, [123]), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, []), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => 123}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{notthis => <<"bzzzz">>}), - {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>, redundant => 123}), - %% backend func throws exception - {error, internal, _} = mongoose_commands:execute(admin, command_one, [<<"throw">>]), - %% backend func returns error - {error, internal, <<"byleco">>} = mongoose_commands:execute(admin, command_one, [<<"error">>]), - % user executes his command - {ok, <<"bzzzz">>} = mongoose_commands:execute(ujid(), command_foruser, #{msg => <<"bzzzz">>}), - % a caller arg - % called by admin - {ok, <<"admin@localhost/zbzzzz">>} = mongoose_commands:execute(admin, - command_withcaller, - #{caller => <<"admin@localhost/z">>, - msg => <<"bzzzz">>}), - % called by user - {ok, <<"zenek@localhost/zbzzzz">>} = mongoose_commands:execute(<<"zenek@localhost">>, - command_withcaller, - #{caller => <<"zenek@localhost/z">>, - msg => <<"bzzzz">>}), - % call by user but jids do not match - {error, denied, _} = mongoose_commands:execute(<<"wacek@localhost">>, - command_withcaller, - #{caller => <<"zenek@localhost/z">>, - msg => <<"bzzzz">>}), - {ok, 30} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>}), - {ok, 18} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>, value => 6}), - ok. - -different_types(_C) -> - mongoose_commands:register(commands_new_temp2()), - {ok, <<"response1">>} = mongoose_commands:execute(admin, command_two, [10, 15]), - {ok, <<"response2">>} = mongoose_commands:execute(admin, command_three, [10, <<"binary">>]), - mongoose_commands:unregister(commands_new_temp2()), - ok. - -errors_are_readable(_C) -> - {error, internal, TextBin} = mongoose_commands:execute(admin, make_error, [<<"oops">>]), - Map = parse_binary_term(TextBin), - [<<"oops">>] = maps:get(args, Map), - admin = maps:get(caller, Map), - error = maps:get(class, Map), - make_error = maps:get(command_name, Map), - <<"oops">> = maps:get(reason, Map), - [_|_] = maps:get(stacktrace, Map), - command_failed = maps:get(what, Map), - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% definitions -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -commands_new() -> - [ - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_noreturn}, - {category, <<"message">>}, - {desc, <<"do nothing and return nothing">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, create}, - {args, [{msg, binary}]}, - {result, ok} - ], - [ - {name, command_foruser}, - {category, <<"another">>}, - {desc, <<"this is available for a user">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {security_policy, [user]}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_withoptargs}, - {category, <<"yetanother">>}, - {desc, <<"this is available for a user">>}, - {module, ?MODULE}, - {function, cmd_one_withvalue}, - {action, read}, - {security_policy, [user]}, - {args, [{msg, binary}]}, - {optargs, [{value, integer, 10}]}, - {result, {nvalue, integer}} - ], - [ - {name, command_withcaller}, - {category, <<"another">>}, - {desc, <<"this has a 'caller' argument, returns caller ++ msg">>}, - {module, ?MODULE}, - {function, cmd_concat}, - {action, create}, - {security_policy, [user]}, - {args, [{caller, binary}, {msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, make_error}, - {category, <<"testing">>}, - {desc, <<"Just to test an error">>}, - {module, erlang}, - {function, error}, - {action, read}, - {args, [{error, binary}]}, - {result, []} - ] - ]. - -commands_new_temp() -> - %% this is to check registering/unregistering commands - [ - [ - {name, command_temp}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, create}, % different action - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one_arity}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}, {whatever, integer}]}, % different arity - {result, {msg, binary}} - ], - [ - {name, command_one_two}, - {category, <<"user">>}, - {subcategory, <<"rosters">>}, % has subcategory - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_temp2}, - {category, <<"user">>}, - {desc, <<"this one specifies identifiers">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [ident]}, - {args, [{ident, integer}, {msg, binary}]}, - {result, {msg, binary}} - ] - ]. - -commands_new_temp2() -> - %% This is for extra test with different arg types - [ - [ - {name, command_two}, - {category, <<"animals">>}, - {desc, <<"some">>}, - {module, ?MODULE}, - {function, the_same_types}, - {action, read}, - {args, [{one, integer}, {two, integer}]}, - {result, {msg, binary}} - ], - [ - {name, command_three}, - {category, <<"music">>}, - {desc, <<"two args, different types">>}, - {module, ?MODULE}, - {function, different_types}, - {action, read}, - {args, [{one, integer}, {two, binary}]}, - {result, {msg, binary}} - ] - ]. - -commands_new_lame() -> - [ - [ - {name, command_one} % missing values - ], - [ - {name, command_one}, - {category, []} %% should be binary - ], - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, andnowforsomethingcompletelydifferent} %% not one of allowed values - ], - [ - {name, command_one}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, delete}, - {args, [{msg, binary}, integer]}, %% args have to be a flat list of named arguments - {result, {msg, binary}} - ], -%% We do not crash if command is already registered because some modules are loaded more then once -%% [ -%% {name, command_one}, %% everything is fine, but it is already registered -%% {category, another}, -%% {desc, "do nothing and return"}, -%% {module, ?MODULE}, -%% {function, cmd_one}, -%% {action, read}, -%% {args, [{msg, binary}]}, -%% {result, {msg, binary}} -%% ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, %% an 'update' command has to specify identifiers - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [1]}, %% ...and they must be atoms... - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_one}, - {category, <<"another">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, update}, - {identifiers, [ident]}, %% ...which are present in args - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_seven}, %% name is different... - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, read}, %% ...but another command with the same category and action and arity is already registered - {args, [{msg, binary}]}, - {result, {msg, binary}} - ], - [ - {name, command_seven}, - {category, <<"user">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, cmd_one}, - {action, delete}, - {security_policy, [wrong]}, % invalid security definition - {args, [{msg, binary}]}, - {result, {msg, binary}} -%% ], -%% [ -%% {name, command_seven}, -%% {category, user}, -%% {desc, "do nothing and return"}, -%% {module, ?MODULE}, -%% {function, cmd_one}, -%% {action, delete}, -%% {security_policy, []}, % invalid security definition -%% {args, [{msg, binary}]}, -%% {result, {msg, binary}} - ] - ]. - -commands_old() -> - [ - #ejabberd_commands{name = command_one, tags = [one], - desc = "do nothing and return", - module = ?MODULE, function = cmd_one, - args = [{msg, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = command_two, tags = [two], - desc = "this returns wrong type", - module = ?MODULE, function = cmd_two, - args = [{msg, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = command_three, tags = [two, three], - desc = "do nothing and return", - module = ?MODULE, function = cmd_three, - args = [{msg, binary}], - result = {res, restuple}} - ]. - -cmd_one(<<"throw">>) -> - C = 12, - <<"A", C/binary>>; -cmd_one(<<"error">>) -> - {error, internal, <<"byleco">>}; -cmd_one(M) -> - M. - -cmd_one_withvalue(_Msg, Value) -> - Value * 3. - -cmd_two(M) -> - M. - -the_same_types(10, 15) -> - <<"response1">>; -the_same_types(_, _) -> - <<"wrong response">>. - -different_types(10, <<"binary">>) -> - <<"response2">>; -different_types(_, _) -> - <<"wrong content">>. - -cmd_concat(A, B) -> - <>. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% utilities -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% this is a bit stupid, but we need a process which would hold ets table -ec_holder() -> - ejabberd_commands:init(), - ejabberd_commands:register_commands(commands_old()), - receive - _ -> ok - end. - -mc_holder() -> - % we have to do it here to avoid race condition and random failures - {ok, Pid} = gen_hook:start_link(), - mongoose_commands:init(), - mongoose_commands:register(commands_new()), - receive - _ -> ok - end, - erlang:exit(Pid, kill). - -command_rules(Commands) -> - #{commands => Commands, argument_restrictions => #{}}. - -checkauth(true, AccessCommands, Auth) -> - B = <<"bzzzz">>, - B = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]); -checkauth(ErrMess, AccessCommands, Auth) -> - B = <<"bzzzz">>, - {error, ErrMess} = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]). - -ujid() -> - <<"zenek@localhost/k">>. -%% #jid{user = <<"zenek">>, server = <<"localhost">>, resource = "k", -%% luser = <<"zenek">>, lserver = <<"localhost">>, lresource = "k"}. - -parse_binary_term(TextBin) -> - {ok, Tokens, _} = erl_scan:string(binary_to_list(TextBin) ++ "."), - {ok, Abstract} = erl_parse:parse_exprs(Tokens), - {value, Value, _} = erl_eval:exprs(Abstract, erl_eval:new_bindings()), - Value. diff --git a/test/commands_backend_SUITE.erl b/test/commands_backend_SUITE.erl deleted file mode 100644 index a32593743d7..00000000000 --- a/test/commands_backend_SUITE.erl +++ /dev/null @@ -1,536 +0,0 @@ -%% @doc This suite tests both old ejabberd_commands module, which is slowly getting deprecated, -%% and the new mongoose_commands implementation. --module(commands_backend_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("eunit/include/eunit.hrl"). - --define(PORT, 5288). --define(HOST, "localhost"). --define(IP, {127,0,0,1}). - --type method() :: string(). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% suite configuration -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -backend_module() -> - mongoose_api_admin. - -all() -> - [ - {group, simple_backend}, - {group, get_advanced_backend}, - {group, post_advanced_backend}, - {group, delete_advanced_backend} - ]. - -groups() -> - [ - {simple_backend, [sequence], - [ - get_simple, - post_simple, - delete_simple, - put_simple - ] - }, - {get_advanced_backend, [sequence], - [ - get_two_args, - get_wrong_path, - get_wrong_arg_number, - get_no_command, - get_wrong_arg_type - ] - }, - {post_advanced_backend, [sequence], - [ - post_simple_with_subcategory, - post_different_arg_order, - post_wrong_arg_number, - post_wrong_arg_name, - post_wrong_arg_type, - post_no_command - ] - }, - {delete_advanced_backend, [sequence], - [ - delete_wrong_arg_order, - delete_wrong_arg_types - ] - }, - {put_advanced_backend, [sequence], - [ - put_wrong_type, - put_wrong_param_type, - put_wrong_bind_type, - put_different_params_order, - put_wrong_binds_order, - put_too_less_params, - put_too_less_binds, - put_wrong_bind_name, - put_wrong_param_name - ] - } - ]. - -setup(Module) -> - meck:unload(), - meck:new(supervisor, [unstick, passthrough, no_link]), - meck:new(gen_hook, []), - meck:new(ejabberd_auth, []), - %% you have to meck some stuff to get it working.... - meck:expect(gen_hook, add_handler, fun(_, _, _, _, _) -> ok end), - meck:expect(gen_hook, run_fold, fun(_, _, _, _) -> {ok, ok} end), - spawn(fun mc_holder/0), - meck:expect(supervisor, start_child, - fun(mongoose_listener_sup, _) -> {ok, self()}; - (A, B) -> meck:passthrough([A, B]) - end), - mongoose_listener_sup:start_link(), - %% HTTP API config - Opts = #{transport => #{num_acceptors => 10, max_connections => 1024}, - protocol => #{}, - port => ?PORT, - ip_tuple => ?IP, - proto => tcp, - handlers => [#{host => "localhost", path => "/api", module => Module}]}, - ejabberd_cowboy:start_listener(Opts). - -teardown() -> - cowboy:stop_listener(ejabberd_cowboy:ref({?PORT, ?IP, tcp})), - mongoose_commands:unregister(commands_new()), - meck:unload(ejabberd_auth), - meck:unload(gen_hook), - meck:unload(supervisor), - mc_holder_proc ! stop, - ok. - -init_per_suite(C) -> - application:ensure_all_started(cowboy), - application:ensure_all_started(jid), - application:ensure_all_started(fusco), - ok = mnesia:start(), - C. - -end_per_suite(C) -> - stopped = mnesia:stop(), - mnesia:delete_schema([node()]), - application:stop(fusco), - application:stop(cowboy), - C. - -init_per_group(_, C) -> - C. - -end_per_group(_, C) -> - C. - -init_per_testcase(_, C) -> - C. - -end_per_testcase(_, C) -> - C. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% Backend side tests -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -get_simple(_Config) -> - Arg = {arg1, <<"bob@localhost">>}, - Base = "/api/users", - ExpectedBody = get_simple_command(element(2, Arg)), - {ok, Response} = request(create_path_with_binds(Base, [Arg]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -delete_simple(_Config) -> - Arg1 = {arg1, <<"ala_ma_kota">>}, - Arg2 = {arg2, 2}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "DELETE", admin), - check_status_code(Response, 204). - -post_simple(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg1, Arg2], - Path = <<"/api/weather">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix()++"/api/weather/" ++ Result)). - -post_simple_with_subcategory(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg2], - Path = <<"/api/weather/10/subcategory">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix()++"/api/weather/10/subcategory/" ++ Result)). - -put_simple(_Config) -> - Binds = [{arg1, <<"username">>}, {arg2,<<"localhost">>}], - Args = [{arg3, <<"newusername">>}], - Base = "/api/users", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Args, admin), - check_status_code(Response, 204). - -get_two_args(_Config) -> - Arg1 = {arg1, 1}, - Arg2 = {arg2, 2}, - Base = "/api/animals", - ExpectedBody = get_two_args_command(element(2, Arg1), element(2, Arg2)), - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_two_args_different_types(_Config) -> - Arg1 = {one, 1}, - Arg2 = {two, <<"mybin">>}, - Base = "/api/books", - ExpectedBody = get_two_args2_command(element(2, Arg1), element(2, Arg2)), - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "GET", admin), - check_status_code(Response, 200), - check_response_body(Response, ExpectedBody). - -get_wrong_path(_Config) -> - Path = <<"/api/animals2/1/2">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_wrong_arg_number(_Config) -> - Path = <<"/api/animals/1/2/3">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_no_command(_Config) -> - Path = <<"/api/unregistered_command/123123">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 404). - -get_wrong_arg_type(_Config) -> - Path = <<"/api/animals/1/wrong">>, - {ok, Response} = request(Path, "GET", admin), - check_status_code(Response, 400). - -post_wrong_arg_number(_Config) -> - Args = [{arg1, 10}, {arg2,2}, {arg3, 100}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 404). - -post_wrong_arg_name(_Config) -> - Args = [{arg11, 10}, {arg2,2}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 400). - -post_wrong_arg_type(_Config) -> - Args = [{arg1, 10}, {arg2,<<"weird binary">>}], - Path = <<"/api/weather">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 400). - -post_different_arg_order(_Config) -> - Arg1 = {arg1, 10}, - Arg2 = {arg2, 2}, - Args = [Arg2, Arg1], - Path = <<"/api/weather">>, - Result = binary_to_list(post_simple_command(element(2, Arg1), element(2, Arg2))), - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 201), - check_location_header(Response, list_to_binary(build_path_prefix() ++"/api/weather/" ++ Result)). - -post_no_command(_Config) -> - Args = [{arg1, 10}, {arg2,2}], - Path = <<"/api/weather/10">>, - {ok, Response} = request(Path, "POST", Args, admin), - check_status_code(Response, 404). - - -delete_wrong_arg_order(_Config) -> - Arg1 = {arg1, <<"ala_ma_kota">>}, - Arg2 = {arg2, 2}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg2, Arg1]), "DELETE", admin), - check_status_code(Response, 400). - -delete_wrong_arg_types(_Config) -> - Arg1 = {arg1, 2}, - Arg2 = {arg2, <<"ala_ma_kota">>}, - Base = "/api/music", - {ok, Response} = request(create_path_with_binds(Base, [Arg1, Arg2]), "DELETE", admin), - check_status_code(Response, 400). - -put_wrong_param_type(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{age, <<"23">>}, {kids, 10}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_wrong_bind_type(_Config) -> - Binds = [{username, <<"username">>}, {domain, 123}], - Parameters = [{age, 23}, {kids, 10}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_different_params_order(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{kids, 2}, {age, 45}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 200). - -put_wrong_binds_order(_Config) -> - Binds = [{domain, <<"domain">>}, {username, <<"username">>}], - Parameters = [{kids, 2}, {age, 30}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_too_less_params(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"domain">>}], - Parameters = [{kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 400). - -put_too_less_binds(_Config) -> - Binds = [{username, <<"username">>}], - Parameters = [{age, 20}, {kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -put_wrong_bind_name(_Config) -> - Binds = [{usersrejm, <<"username">>}, {domain, <<"localhost">>}], - Parameters = [{age, 20}, {kids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -put_wrong_param_name(_Config) -> - Binds = [{username, <<"username">>}, {domain, <<"localhost">>}], - Parameters = [{age, 20}, {srids, 3}], - Base = "/api/dragons", - {ok, Response} = request(create_path_with_binds(Base, Binds), "PUT", Parameters, admin), - check_status_code(Response, 404). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% definitions -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -commands_admin() -> - [ - [ - {name, get_simple}, - {category, <<"users">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_simple_command}, - {action, read}, - {identifiers, []}, - {args, [{arg1, binary}]}, - {result, {result, binary}} - ], - [ - {name, get_advanced}, - {category, <<"animals">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_two_args_command}, - {action, read}, - {identifiers, []}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, get_advanced2}, - {category, <<"books">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, get_two_args2_command}, - {action, read}, - {identifiers, []}, - {args, [{one, integer}, {two, binary}]}, - {result, {result, integer}} - ], - [ - {name, post_simple}, - {category, <<"weather">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, post_simple_command}, - {action, create}, - {identifiers, []}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, post_simple2}, - {category, <<"weather">>}, - {subcategory, <<"subcategory">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, post_simple_command}, - {action, create}, - {identifiers, [arg1]}, - {args, [{arg1, integer}, {arg2, integer}]}, - {result, {result, binary}} - ], - [ - {name, delete_simple}, - {category, <<"music">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, delete_simple_command}, - {action, delete}, - {identifiers, []}, - {args, [{arg1, binary}, {arg2, integer}]}, - {result, ok} - ], - [ - {name, put_simple}, - {category, <<"users">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, put_simple_command}, - {action, update}, - {args, [{arg1, binary}, {arg2, binary}, {arg3, binary}]}, - {identifiers, [arg1, arg2]}, - {result, ok} - ], - [ - {name, put_advanced}, - {category, <<"dragons">>}, - {desc, <<"do nothing and return">>}, - {module, ?MODULE}, - {function, put_advanced_command}, - {action, update}, - {args, [{username, binary}, - {domain, binary}, - {age, integer}, - {kids, integer}]}, - {identifiers, [username, domain]}, - {result, ok} - ] - ]. - -commands_new() -> - commands_admin(). - -%% admin command funs -get_simple_command(<<"bob@localhost">>) -> - <<"bob is OK">>. - -get_two_args_command(1, 2) -> - <<"all is working">>. - -get_two_args2_command(X, B) when is_integer(X) and is_binary(B) -> - 100. - -post_simple_command(_X, 2) -> - <<"new_resource">>. - -delete_simple_command(Binary, 2) when is_binary(Binary) -> - 10. - -put_simple_command(_Arg1, _Arg2, _Arg3) -> - ok. - -put_advanced_command(Arg1, Arg2, Arg3, Arg4) when is_binary(Arg1) and is_binary(Arg2) - and is_integer(Arg3) and is_integer(Arg4) -> - ok. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% utilities -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -build_path_prefix() -> - "http://" ++ ?HOST ++ ":" ++ integer_to_list(?PORT). - -maybe_add_body([]) -> - []; -maybe_add_body(Args) -> - jiffy:encode(maps:from_list(Args)). - -maybe_add_accepted_headers("POST") -> - accepted_headers(); -maybe_add_accepted_headers("PUT") -> - accepted_headers(); -maybe_add_accepted_headers(_) -> - []. - -accepted_headers() -> - [{<<"Content-Type">>, <<"application/json">>}, {<<"Accept">>, <<"application/json">>}]. - -maybe_add_auth_header(admin) -> - []. - --spec create_path_with_binds(string(), list()) -> binary(). -create_path_with_binds(Base, ArgList) when is_list(ArgList) -> - list_to_binary( - lists:flatten(Base ++ ["/" ++ to_list(ArgValue) - || {ArgName, ArgValue} <- ArgList])). - -to_list(Int) when is_integer(Int) -> - integer_to_list(Int); -to_list(Float) when is_float(Float) -> - float_to_list(Float); -to_list(Bin) when is_binary(Bin) -> - binary_to_list(Bin); -to_list(Atom) when is_atom(Atom) -> - atom_to_list(Atom); -to_list(Other) -> - Other. - --spec request(binary(), method(), admin | {{binary(), binary()}, boolean()}) -> any. -request(Path, "GET", Entity) -> - request(Path, "GET", [], Entity); -request(Path, "DELETE", Entity) -> - request(Path, "DELETE", [], Entity). - --spec request(binary(), method(), list({atom(), any()}), - {headers, list()} | admin | {{binary(), binary()}, boolean()}) -> any. -do_request(Path, Method, Body, {headers, Headers}) -> - {ok, Pid} = fusco:start_link("http://"++ ?HOST ++ ":" ++ integer_to_list(?PORT), []), - R = fusco:request(Pid, Path, Method, Headers, Body, 5000), - fusco:disconnect(Pid), - teardown(), - R. - -request(Path, Method, BodyData, admin) -> - ct:pal("~p, ~p, ~p", [Path, Method, BodyData]), - setup(backend_module()), - Body = maybe_add_body(BodyData), - AcceptHeader = maybe_add_accepted_headers(Method), - do_request(Path, Method, Body, {headers, AcceptHeader}). - -mc_holder() -> - erlang:register(mc_holder_proc, self()), - mongoose_commands:init(), - mongoose_commands:register(commands_new()), - receive - _ -> ok - end, - erlang:unregister(mc_holder_proc). - -check_status_code(Response, Code) when is_integer(Code) -> - {{ResCode, _}, _, _, _, _} = Response, - ?assertEqual(Code, binary_to_integer(ResCode)); -check_status_code(_R, Code) -> - ?assertEqual(Code, not_a_number). - -check_response_body(Response, ExpectedBody) -> - {_, _, Body, _ , _} = Response, - ?assertEqual(binary_to_list(Body), "\"" ++ binary_to_list(ExpectedBody) ++ "\""). - -check_location_header(Response, Path) -> - {_, Headers, _, _ , _} = Response, - Location = proplists:get_value(<<"location">>, Headers), - ?assertEqual(Path, Location). diff --git a/test/mongoose_api_common_SUITE.erl b/test/mongoose_api_common_SUITE.erl deleted file mode 100644 index eb47778091c..00000000000 --- a/test/mongoose_api_common_SUITE.erl +++ /dev/null @@ -1,72 +0,0 @@ --module(mongoose_api_common_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("eunit/include/eunit.hrl"). - - --define(aq(E, V), ( - [ct:fail("ASSERT EQUAL~n\tExpected ~p~n\tActual ~p~n", [(E), (V)]) - || (E) =/= (V)] - )). - -all() -> - [url_is_correct_for_create_command, - url_is_correct_for_read_command, - url_is_correct_for_read_command_with_subcategory]. - -url_is_correct_for_create_command(_) -> - Cmd = create_cmd(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host">>, Url). - -url_is_correct_for_read_command(_) -> - Cmd = read_cmd(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host">>, Url). - -url_is_correct_for_read_command_with_subcategory(_) -> - Cmd = read_cmd2(), - Url = mongoose_api_common:create_admin_url_path(Cmd), - ?aq(<<"/users/:host/rosters">>, Url). - -create_cmd() -> - Props = [ - {name, registeruser}, - {category, <<"users">>}, - {desc, <<"Register a user">>}, - {module, ?MODULE}, - {function, register}, - {action, create}, - {args, [{user, binary}, {host, binary}, {password, binary}]}, - {identifiers, [host]}, - {result, {msg, binary}} - ], - mongoose_commands:new(Props). - -read_cmd() -> - Props = [ - {name, listusers}, - {category, <<"users">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - mongoose_commands:new(Props). - -read_cmd2() -> - Props = [ - {name, listusers}, - {category, <<"users">>}, - {subcategory, <<"rosters">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - mongoose_commands:new(Props). - From a244b479b663663d4e96dac7b43ce2c59cd237a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:23:26 +0200 Subject: [PATCH 039/117] Remove mod_*commands from big tests --- big_tests/tests/inbox_helper.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/big_tests/tests/inbox_helper.erl b/big_tests/tests/inbox_helper.erl index 32582acedb5..f8963d032fc 100644 --- a/big_tests/tests/inbox_helper.erl +++ b/big_tests/tests/inbox_helper.erl @@ -163,8 +163,7 @@ insert_parallels(Gs) -> inbox_modules(Backend) -> [ - {mod_inbox, inbox_opts(Backend)}, - {mod_inbox_commands, #{}} + {mod_inbox, inbox_opts(Backend)} ]. muclight_modules() -> From cdb4146ede2b93ed42ebc530d2b4b5a845cb2c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:25:44 +0200 Subject: [PATCH 040/117] Remove mod_*commands This functionality is now provided by mongoose_admin_api --- rel/files/mongooseim.toml | 6 - src/inbox/mod_inbox.erl | 2 +- src/inbox/mod_inbox_commands.erl | 61 --- src/mod_commands.erl | 457 ----------------------- src/mod_muc_commands.erl | 174 --------- src/muc_light/mod_muc_light_commands.erl | 224 ----------- 6 files changed, 1 insertion(+), 923 deletions(-) delete mode 100644 src/inbox/mod_inbox_commands.erl delete mode 100644 src/mod_commands.erl delete mode 100644 src/mod_muc_commands.erl delete mode 100644 src/muc_light/mod_muc_light_commands.erl diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index c5f9b33b5e1..79c8c382d03 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -217,16 +217,10 @@ [modules.mod_disco] users_can_see_hidden_services = false -[modules.mod_commands] - {{#mod_cache_users}} [modules.mod_cache_users] {{{mod_cache_users}}} {{/mod_cache_users}} -[modules.mod_muc_commands] - -[modules.mod_muc_light_commands] - {{#mod_last}} [modules.mod_last] {{{mod_last}}} diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index bff15a8e4d8..b87dec18f6f 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -165,7 +165,7 @@ process_inbox_boxes(Config = #{boxes := Boxes}) -> %% Cleaner gen_server callbacks start_cleaner(HostType, #{bin_ttl := TTL, bin_clean_after := Interval}) -> Name = gen_mod:get_module_proc(HostType, ?MODULE), - WOpts = #{host_type => HostType, action => fun mod_inbox_commands:flush_global_bin/2, + WOpts = #{host_type => HostType, action => fun mod_inbox_api:flush_global_bin/2, opts => TTL, interval => Interval}, MFA = {mongoose_collector, start_link, [Name, WOpts]}, ChildSpec = {Name, MFA, permanent, 5000, worker, [?MODULE]}, diff --git a/src/inbox/mod_inbox_commands.erl b/src/inbox/mod_inbox_commands.erl deleted file mode 100644 index 27b1e4022d2..00000000000 --- a/src/inbox/mod_inbox_commands.erl +++ /dev/null @@ -1,61 +0,0 @@ --module(mod_inbox_commands). - --behaviour(gen_mod). - -%% gen_mod --export([start/2, stop/1, supported_features/0]). - --export([flush_user_bin/3, flush_global_bin/2]). --ignore_xref([flush_user_bin/3, flush_global_bin/2]). - -%% Initialisation --spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%% Clean commands -commands() -> - [ - [{name, inbox_flush_user_bin}, - {category, <<"inbox">>}, - {subcategory, <<"bin">>}, - {desc, <<"Empty the bin for a user">>}, - {module, ?MODULE}, - {function, flush_user_bin}, - {action, delete}, - {identifiers, [domain, name, since]}, - {args, [{domain, binary}, - {name, binary}, - {since, integer}]}, - {result, {num, integer}}], - [{name, inbox_flush_global_bin}, - {category, <<"inbox">>}, - {subcategory, <<"bin">>}, - {desc, <<"Empty the inbox bin globally">>}, - {module, ?MODULE}, - {function, flush_global_bin}, - {action, delete}, - {identifiers, [host_type, since]}, - {args, [{host_type, binary}, - {since, integer}]}, - {result, {num, integer}}] - ]. - -flush_user_bin(Domain, Name, Days) -> - JID = jid:make_bare(Name, Domain), - Res = mod_inbox_api:flush_user_bin(JID, Days), - format_result(Res). - -flush_global_bin(HostType, Days) -> - Res = mod_inbox_api:flush_global_bin(HostType, Days), - format_result(Res). - -format_result({ok, Count}) -> Count; -format_result({_, ErrMsg}) -> {error, bad_request, ErrMsg}. diff --git a/src/mod_commands.erl b/src/mod_commands.erl deleted file mode 100644 index 6223a65a275..00000000000 --- a/src/mod_commands.erl +++ /dev/null @@ -1,457 +0,0 @@ --module(mod_commands). --author('bartlomiej.gorny@erlang-solutions.com'). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/0, stop/0, supported_features/0, - start/2, stop/1, - register/3, - unregister/2, - registered_commands/0, - registered_users/1, - change_user_password/3, - list_sessions/1, - list_contacts/1, - add_contact/2, - add_contact/3, - add_contact/4, - delete_contacts/2, - delete_contact/2, - subscription/3, - set_subscription/3, - kick_session/3, - get_recent_messages/3, - get_recent_messages/4, - send_message/3, - send_stanza/1 - ]). - --ignore_xref([add_contact/2, add_contact/3, add_contact/4, change_user_password/3, - delete_contact/2, delete_contacts/2, get_recent_messages/3, - get_recent_messages/4, kick_session/3, list_contacts/1, - list_sessions/1, register/3, registered_commands/0, registered_users/1, - send_message/3, send_stanza/1, set_subscription/3, start/0, stop/0, - subscription/3, unregister/2]). - --include("mongoose.hrl"). --include("jlib.hrl"). --include("mongoose_rsm.hrl"). --include("session.hrl"). - -start() -> - mongoose_commands:register(commands()). - -stop() -> - mongoose_commands:unregister(commands()). - -start(_, _) -> start(). -stop(_) -> stop(). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%%% -%%% mongoose commands -%%% - -commands() -> - [ - [ - {name, list_methods}, - {category, <<"commands">>}, - {desc, <<"List commands">>}, - {module, ?MODULE}, - {function, registered_commands}, - {action, read}, - {args, []}, - {result, []} - ], - [ - {name, list_users}, - {category, <<"users">>}, - {desc, <<"List registered users on this host">>}, - {module, ?MODULE}, - {function, registered_users}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - [ - {name, register_user}, - {category, <<"users">>}, - {desc, <<"Register a user">>}, - {module, ?MODULE}, - {function, register}, - {action, create}, - {args, [{host, binary}, {username, binary}, {password, binary}]}, - {result, {msg, binary}} - ], - [ - {name, unregister_user}, - {category, <<"users">>}, - {desc, <<"UnRegister a user">>}, - {module, ?MODULE}, - {function, unregister}, - {action, delete}, - {args, [{host, binary}, {user, binary}]}, - {result, ok} - ], - [ - {name, list_sessions}, - {category, <<"sessions">>}, - {desc, <<"Get session list">>}, - {module, ?MODULE}, - {function, list_sessions}, - {action, read}, - {args, [{host, binary}]}, - {result, []} - ], - [ - {name, kick_user}, - {category, <<"sessions">>}, - {desc, <<"Terminate user connection">>}, - {module, ?MODULE}, - {function, kick_session}, - {action, delete}, - {args, [{host, binary}, {user, binary}, {res, binary}]}, - {result, ok} - ], - [ - {name, list_contacts}, - {category, <<"contacts">>}, - {desc, <<"Get roster">>}, - {module, ?MODULE}, - {function, list_contacts}, - {action, read}, - {args, [{caller, binary}]}, - {result, []} - ], - [ - {name, add_contact}, - {category, <<"contacts">>}, - {desc, <<"Add a contact to roster">>}, - {module, ?MODULE}, - {function, add_contact}, - {action, create}, - {args, [{caller, binary}, {jid, binary}]}, - {result, ok} - ], - [ - {name, subscription}, - {category, <<"contacts">>}, - {desc, <<"Send out a subscription request">>}, - {module, ?MODULE}, - {function, subscription}, - {action, update}, - {identifiers, [caller, jid]}, - % caller has to be in identifiers, otherwise it breaks admin rest api - {args, [{caller, binary}, {jid, binary}, {action, binary}]}, - {result, ok} - ], - [ - {name, set_subscription}, - {category, <<"contacts">>}, - {subcategory, <<"manage">>}, - {desc, <<"Set / unset mutual subscription">>}, - {module, ?MODULE}, - {function, set_subscription}, - {action, update}, - {identifiers, [caller, jid]}, - {args, [{caller, binary}, {jid, binary}, {action, binary}]}, - {result, ok} - ], - [ - {name, delete_contact}, - {category, <<"contacts">>}, - {desc, <<"Remove a contact from roster">>}, - {module, ?MODULE}, - {function, delete_contact}, - {action, delete}, - {args, [{caller, binary}, {jid, binary}]}, - {result, ok} - ], - [ - {name, delete_contacts}, - {category, <<"contacts">>}, - {subcategory, <<"multiple">>}, - {desc, <<"Remove provided contacts from roster">>}, - {module, ?MODULE}, - {function, delete_contacts}, - {action, delete}, - {args, [{caller, binary}, {jids, [binary]}]}, - {result, []} - ], - [ - {name, send_message}, - {category, <<"messages">>}, - {desc, <<"Send chat message from to">>}, - {module, ?MODULE}, - {function, send_message}, - {action, create}, - {args, [{caller, binary}, {to, binary}, {body, binary}]}, - {result, ok} - ], - [ - {name, send_stanza}, - {category, <<"stanzas">>}, - {desc, <<"Send an arbitrary stanza">>}, - {module, ?MODULE}, - {function, send_stanza}, - {action, create}, - {args, [{stanza, binary}]}, - {result, ok} - ], - [ - {name, get_last_messages_with_everybody}, - {category, <<"messages">>}, - {desc, <<"Get n last messages from archive, optionally before a certain date (unixtime)">>}, - {module, ?MODULE}, - {function, get_recent_messages}, - {action, read}, - {args, [{caller, binary}]}, - {optargs, [{before, integer, 0}, {limit, integer, 100}]}, - {result, []} - ], - [ - {name, get_last_messages}, - {category, <<"messages">>}, - {desc, <<"Get n last messages to/from given contact, with limit and date">>}, - {module, ?MODULE}, - {function, get_recent_messages}, - {action, read}, - {args, [{caller, binary}, {with, binary}]}, - {optargs, [{before, integer, 0}, {limit, integer, 100}]}, - {result, []} - ], - [ - {name, change_password}, - {category, <<"users">>}, - {desc, <<"Change user password">>}, - {module, ?MODULE}, - {function, change_user_password}, - {action, update}, - {identifiers, [host, user]}, - {args, [{host, binary}, {user, binary}, {newpass, binary}]}, - {result, ok} - ] - ]. - -kick_session(Host, User, Resource) -> - case mongoose_session_api:kick_session(User, Host, Resource, <<"kicked">>) of - {ok, Msg} -> Msg; - {no_session, Msg} -> {error, not_found, Msg} - end. - -list_sessions(Host) -> - mongoose_session_api:list_resources(Host). - -registered_users(Host) -> - mongoose_account_api:list_users(Host). - -register(Host, User, Password) -> - Res = mongoose_account_api:register_user(User, Host, Password), - format_account_result(Res). - -unregister(Host, User) -> - Res = mongoose_account_api:unregister_user(User, Host), - format_account_result(Res). - -change_user_password(Host, User, Password) -> - Res = mongoose_account_api:change_password(User, Host, Password), - format_account_result(Res). - -format_account_result({ok, Msg}) -> iolist_to_binary(Msg); -format_account_result({empty_password, Msg}) -> {error, bad_request, Msg}; -format_account_result({invalid_jid, Msg}) -> {error, bad_request, Msg}; -format_account_result({not_allowed, Msg}) -> {error, denied, Msg}; -format_account_result({exists, Msg}) -> {error, denied, Msg}; -format_account_result({cannot_register, Msg}) -> {error, internal, Msg}. - -send_message(From, To, Body) -> - case mongoose_stanza_helper:parse_from_to(From, To) of - {ok, FromJID, ToJID} -> - Packet = mongoose_stanza_helper:build_message(From, To, Body), - do_send_packet(FromJID, ToJID, Packet); - Error -> - Error - end. - -send_stanza(BinStanza) -> - case exml:parse(BinStanza) of - {ok, Packet} -> - From = exml_query:attr(Packet, <<"from">>), - To = exml_query:attr(Packet, <<"to">>), - case mongoose_stanza_helper:parse_from_to(From, To) of - {ok, FromJID, ToJID} -> - do_send_packet(FromJID, ToJID, Packet); - {error, missing} -> - {error, bad_request, "both from and to are required"}; - {error, type_error, E} -> - {error, type_error, E} - end; - {error, Reason} -> - {error, bad_request, io_lib:format("Malformed stanza: ~p", [Reason])} - end. - -do_send_packet(From, To, Packet) -> - case mongoose_domain_api:get_domain_host_type(From#jid.lserver) of - {ok, HostType} -> - Acc = mongoose_acc:new(#{location => ?LOCATION, - host_type => HostType, - lserver => From#jid.lserver, - element => Packet}), - Acc1 = mongoose_hooks:user_send_packet(Acc, From, To, Packet), - ejabberd_router:route(From, To, Acc1), - ok; - {error, not_found} -> - {error, unknown_domain} - end. - -list_contacts(Caller) -> - case mod_roster_api:list_contacts(jid:from_binary(Caller)) of - {ok, Rosters} -> - [roster_info(mod_roster:item_to_map(R)) || R <- Rosters]; - Error -> - skip_result_msg(Error) - end. - -roster_info(M) -> - Jid = jid:to_binary(maps:get(jid, M)), - #{subscription := Sub, ask := Ask} = M, - #{jid => Jid, subscription => Sub, ask => Ask}. - -add_contact(Caller, JabberID) -> - add_contact(Caller, JabberID, <<"">>, []). - -add_contact(Caller, JabberID, Name) -> - add_contact(Caller, JabberID, Name, []). - -add_contact(Caller, Other, Name, Groups) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJid, OtherJid} -> - Res = mod_roster_api:add_contact(CallerJid, OtherJid, Name, Groups), - skip_result_msg(Res); - E -> - E - end. - -delete_contacts(Caller, ToDelete) -> - maybe_delete_contacts(Caller, ToDelete, []). - -maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted; -maybe_delete_contacts(Caller, [H | T], NotDeleted) -> - case delete_contact(Caller, H) of - ok -> - maybe_delete_contacts(Caller, T, NotDeleted); - _Error -> - maybe_delete_contacts(Caller, T, NotDeleted ++ [H]) - end. - -delete_contact(Caller, Other) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - Res = mod_roster_api:delete_contact(CallerJID, OtherJID), - skip_result_msg(Res); - E -> - E - end. - -registered_commands() -> - Items = collect_commands(), - sort_commands(Items). - -sort_commands(Items) -> - WithKey = [{get_sorting_key(Item), Item} || Item <- Items], - Sorted = lists:keysort(1, WithKey), - [Item || {_Key, Item} <- Sorted]. - -get_sorting_key(Item) -> - maps:get(path, Item). - -collect_commands() -> - [#{name => mongoose_commands:name(C), - category => mongoose_commands:category(C), - action => mongoose_commands:action(C), - method => mongoose_api_common:action_to_method(mongoose_commands:action(C)), - desc => mongoose_commands:desc(C), - args => format_args(mongoose_commands:args(C)), - path => mongoose_api_common:create_admin_url_path(C) - } || C <- mongoose_commands:list(admin)]. - -format_args(Args) -> - maps:from_list([{term_as_binary(Name), term_as_binary(rewrite_type(Type))} - || {Name, Type} <- Args]). - -%% binary is useful internally, but could confuse a regular user -rewrite_type(binary) -> string; -rewrite_type(Type) -> Type. - -term_as_binary(X) -> - iolist_to_binary(io_lib:format("~p", [X])). - -get_recent_messages(Caller, Before, Limit) -> - get_recent_messages(Caller, undefined, Before, Limit). - -get_recent_messages(Caller, With, Before, Limit) -> - Before2 = maybe_seconds_to_microseconds(Before), - Res = mongoose_stanza_api:lookup_recent_messages(Caller, With, Before2, Limit), - lists:map(fun row_to_map/1, Res). - -maybe_seconds_to_microseconds(X) when is_number(X) -> - X * 1000000; -maybe_seconds_to_microseconds(X) -> - X. - --spec row_to_map(mod_mam:message_row()) -> map(). -row_to_map(#{id := Id, jid := From, packet := Msg}) -> - Jbin = jid:to_binary(From), - {Msec, _} = mod_mam_utils:decode_compact_uuid(Id), - MsgId = case xml:get_tag_attr(<<"id">>, Msg) of - {value, MId} -> MId; - false -> <<"">> - end, - Body = exml_query:path(Msg, [{element, <<"body">>}, cdata]), - #{sender => Jbin, timestamp => round(Msec / 1000000), message_id => MsgId, - body => Body}. - -subscription(Caller, Other, Action) -> - case decode_action(Action) of - error -> - {error, bad_request, <<"invalid action">>}; - Act -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - Res = mod_roster_api:subscription(CallerJID, OtherJID, Act), - skip_result_msg(Res); - E -> - E - end - end. - -decode_action(<<"subscribe">>) -> subscribe; -decode_action(<<"subscribed">>) -> subscribed; -decode_action(_) -> error. - -set_subscription(Caller, Other, Action) -> - case mongoose_stanza_helper:parse_from_to(Caller, Other) of - {ok, CallerJID, OtherJID} -> - case decode_both_sub_action(Action) of - error -> - {error, bad_request, <<"invalid action">>}; - ActionDecoded -> - Res = mod_roster_api:set_mutual_subscription(CallerJID, OtherJID, - ActionDecoded), - skip_result_msg(Res) - end; - E -> - E - end. - -decode_both_sub_action(<<"connect">>) -> connect; -decode_both_sub_action(<<"disconnect">>) -> disconnect; -decode_both_sub_action(_) -> error. - -skip_result_msg({ok, _Msg}) -> ok; -skip_result_msg({ErrCode, _Msg}) -> {error, ErrCode}. diff --git a/src/mod_muc_commands.erl b/src/mod_muc_commands.erl deleted file mode 100644 index acc299fdca2..00000000000 --- a/src/mod_muc_commands.erl +++ /dev/null @@ -1,174 +0,0 @@ -%%============================================================================== -%% Copyright 2016 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% Author: Joseph Yiasemides -%% Description: Administration commands for Mult-user Chat (MUC) -%%============================================================================== - --module(mod_muc_commands). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/2, stop/1, supported_features/0]). - --export([create_instant_room/4]). --export([invite_to_room/5]). --export([send_message_to_room/4]). --export([kick_user_from_room/3]). - --include_lib("jid/include/jid.hrl"). - --ignore_xref([create_instant_room/4, invite_to_room/5, kick_user_from_room/3, - send_message_to_room/4]). - -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -commands() -> - [ - [{name, create_muc_room}, - {category, <<"mucs">>}, - {desc, <<"Create a MUC room.">>}, - {module, ?MODULE}, - {function, create_instant_room}, - {action, create}, - {identifiers, [domain]}, - {args, - %% The argument `domain' is what we normally term the XMPP - %% domain, `name' is the room name, `owner' is the XMPP entity - %% that would normally request an instant MUC room. - [{domain, binary}, - {name, binary}, - {owner, binary}, - {nick, binary}]}, - {result, {name, binary}}], - - [{name, invite_to_muc_room}, - {category, <<"mucs">>}, - {subcategory, <<"participants">>}, - {desc, <<"Send a MUC room invite from one user to another.">>}, - {module, ?MODULE}, - {function, invite_to_room}, - {action, create}, - {identifiers, [domain, name]}, - {args, - [{domain, binary}, - {name, binary}, - {sender, binary}, - {recipient, binary}, - {reason, binary} - ]}, - {result, ok}], - - [{name, send_message_to_room}, - {category, <<"mucs">>}, - {subcategory, <<"messages">>}, - {desc, <<"Send a message to a MUC room from a given user.">>}, - {module, ?MODULE}, - {function, send_message_to_room}, - {action, create}, - {identifiers, [domain, name]}, - {args, - [{domain, binary}, - {name, binary}, - {from, binary}, - {body, binary} - ]}, - {result, ok}], - - [{name, kick_user_from_room}, - {category, <<"mucs">>}, - {desc, - <<"Kick a user from a MUC room (on behalf of a moderator).">>}, - {module, ?MODULE}, - {function, kick_user_from_room}, - {action, delete}, - {identifiers, [domain, name, nick]}, - {args, - [{domain, binary}, - {name, binary}, - {nick, binary} - ]}, - {result, ok}] - - ]. - -create_instant_room(Domain, Name, Owner, Nick) -> - case jid:binary_to_bare(Owner) of - error -> - error; - OwnerJID -> - #jid{luser = RName, lserver = MUCServer} = room_jid(Domain, Name), - case mod_muc_api:create_instant_room(MUCServer, RName, OwnerJID, Nick) of - {ok, #{title := RName}} -> RName; - Error -> make_rest_error(Error) - end - end. - -invite_to_room(Domain, Name, Sender, Recipient, Reason) -> - case mongoose_stanza_helper:parse_from_to(Sender, Recipient) of - {ok, SenderJID, RecipientJID} -> - RoomJID = room_jid(Domain, Name), - case mod_muc_api:invite_to_room(RoomJID, SenderJID, RecipientJID, Reason) of - {ok, _} -> - ok; - Error -> - make_rest_error(Error) - end; - Error -> - Error - end. - -send_message_to_room(Domain, Name, Sender, Message) -> - RoomJID = room_jid(Domain, Name), - case jid:from_binary(Sender) of - error -> - error; - SenderJID -> - mod_muc_api:send_message_to_room(RoomJID, SenderJID, Message) - end. - -kick_user_from_room(Domain, Name, Nick) -> - Reason = <<"User kicked from the admin REST API">>, - RoomJID = room_jid(Domain, Name), - case mod_muc_api:kick_user_from_room(RoomJID, Nick, Reason) of - {ok, _} -> - ok; - Error -> - make_rest_error(Error) - end. - -%%-------------------------------------------------------------------- -%% Ancillary -%%-------------------------------------------------------------------- - --spec room_jid(jid:lserver(), binary()) -> jid:jid() | error. -room_jid(Domain, Name) -> - {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), - MUCDomain = mod_muc:server_host_to_muc_host(HostType, Domain), - jid:make(Name, MUCDomain, <<>>). - -make_rest_error({room_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({user_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({moderator_not_found, ErrMsg}) -> {error, not_found, ErrMsg}; -make_rest_error({internal, ErrMsg}) -> {error, internal, ErrMsg}. diff --git a/src/muc_light/mod_muc_light_commands.erl b/src/muc_light/mod_muc_light_commands.erl deleted file mode 100644 index 129860b283b..00000000000 --- a/src/muc_light/mod_muc_light_commands.erl +++ /dev/null @@ -1,224 +0,0 @@ -%%============================================================================== -%% Copyright 2016 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%% -%% Author: Joseph Yiasemides -%% Description: Administration commands for MUC Light -%%============================================================================== - --module(mod_muc_light_commands). - --behaviour(gen_mod). --behaviour(mongoose_module_metrics). - --export([start/2, stop/1, supported_features/0]). - --export([create_unique_room/4]). --export([create_identifiable_room/5]). --export([send_message/4]). --export([invite_to_room/4]). --export([delete_room/2]). --export([change_room_config/5]). - --ignore_xref([create_identifiable_room/5, create_unique_room/4, delete_room/2, - invite_to_room/4, send_message/4, change_room_config/5]). - -%%-------------------------------------------------------------------- -%% `gen_mod' callbacks -%%-------------------------------------------------------------------- - -start(_, _) -> - mongoose_commands:register(commands()). - -stop(_) -> - mongoose_commands:unregister(commands()). - --spec supported_features() -> [atom()]. -supported_features() -> - [dynamic_domains]. - -%%-------------------------------------------------------------------- -%% Interface descriptions -%%-------------------------------------------------------------------- - -commands() -> - - [ - [{name, create_muc_light_room}, - {category, <<"muc-lights">>}, - {desc, <<"Create a MUC Light room with unique username part in JID.">>}, - {module, ?MODULE}, - {function, create_unique_room}, - {action, create}, - {identifiers, [domain]}, - {args, - [ - %% The parent `domain' under which MUC Light is - %% configured. - {domain, binary}, - {name, binary}, - {owner, binary}, - {subject, binary} - ]}, - {result, {name, binary}}], - - [{name, create_identifiable_muc_light_room}, - {category, <<"muc-lights">>}, - {desc, <<"Create a MUC Light room with user-provided username part in JID">>}, - {module, ?MODULE}, - {function, create_identifiable_room}, - {action, update}, - {identifiers, [domain]}, - {args, - [{domain, binary}, - {id, binary}, - {name, binary}, - {owner, binary}, - {subject, binary} - ]}, - {result, {id, binary}}], - - [{name, change_muc_light_room_configuration}, - {category, <<"muc-lights">>}, - {subcategory, <<"config">>}, - {desc, <<"Change configuration of MUC Light room.">>}, - {module, ?MODULE}, - {function, change_room_config}, - {action, update}, - {identifiers, [domain]}, - {args, - [ - {domain, binary}, - {id, binary}, - {name, binary}, - {user, binary}, - {subject, binary} - ]}, - {result, ok}], - - [{name, invite_to_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"participants">>}, - {desc, <<"Invite to a MUC Light room.">>}, - {module, ?MODULE}, - {function, invite_to_room}, - {action, create}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary}, - {sender, binary}, - {recipient, binary} - ]}, - {result, ok}], - - [{name, send_message_to_muc_light_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"messages">>}, - {desc, <<"Send a message to a MUC Light room.">>}, - {module, ?MODULE}, - {function, send_message}, - {action, create}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary}, - {from, binary}, - {body, binary} - ]}, - {result, ok}], - - [{name, delete_room}, - {category, <<"muc-lights">>}, - {subcategory, <<"management">>}, - {desc, <<"Delete a MUC Light room.">>}, - {module, ?MODULE}, - {function, delete_room}, - {action, delete}, - {identifiers, [domain, id]}, - {args, - [{domain, binary}, - {id, binary} - ]}, - {result, ok}] - ]. - - -%%-------------------------------------------------------------------- -%% Internal procedures -%%-------------------------------------------------------------------- - --spec create_unique_room(jid:server(), jid:user(), jid:literal_jid(), binary()) -> - jid:literal_jid() | {error, not_found | denied, iolist()}. -create_unique_room(MUCServer, RoomName, Creator, Subject) -> - CreatorJID = jid:from_binary(Creator), - case mod_muc_light_api:create_room(MUCServer, CreatorJID, RoomName, Subject) of - {ok, #{jid := JID}} -> jid:to_binary(JID); - Error -> format_err_result(Error) - end. - --spec create_identifiable_room(jid:server(), jid:user(), binary(), - jid:literal_jid(), binary()) -> - jid:literal_jid() | {error, not_found | denied, iolist()}. -create_identifiable_room(MUCServer, Identifier, RoomName, Creator, Subject) -> - CreatorJID = jid:from_binary(Creator), - case mod_muc_light_api:create_room(MUCServer, Identifier, CreatorJID, RoomName, Subject) of - {ok, #{jid := JID}} -> jid:to_binary(JID); - Error -> format_err_result(Error) - end. - --spec invite_to_room(jid:server(), jid:user(), jid:literal_jid(), jid:literal_jid()) -> - ok | {error, not_found | denied, iolist()}. -invite_to_room(MUCServer, RoomID, Sender, Recipient) -> - SenderJID = jid:from_binary(Sender), - RecipientJID = jid:from_binary(Recipient), - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:invite_to_room(RoomJID, SenderJID, RecipientJID), - format_result_no_msg(Result). - --spec change_room_config(jid:server(), jid:user(), binary(), jid:literal_jid(), binary()) -> - ok | {error, not_found | denied, iolist()}. -change_room_config(MUCServer, RoomID, RoomName, User, Subject) -> - UserJID = jid:from_binary(User), - RoomJID = jid:make_bare(RoomID, MUCServer), - Config = #{<<"roomname">> => RoomName, <<"subject">> => Subject}, - Result = mod_muc_light_api:change_room_config(RoomJID, UserJID, Config), - format_result_no_msg(Result). - --spec send_message(jid:server(), jid:user(), jid:literal_jid(), binary()) -> - ok | {error, not_found | denied, iolist()}. -send_message(MUCServer, RoomID, Sender, Message) -> - SenderJID = jid:from_binary(Sender), - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:send_message(RoomJID, SenderJID, Message), - format_result_no_msg(Result). - --spec delete_room(jid:server(), jid:user()) -> - ok | {error, not_found, iolist()}. -delete_room(MUCServer, RoomID) -> - RoomJID = jid:make_bare(RoomID, MUCServer), - Result = mod_muc_light_api:delete_room(RoomJID), - format_result_no_msg(Result). - -format_result_no_msg({ok, _}) -> ok; -format_result_no_msg(Res) -> format_err_result(Res). - -format_err_result({ResStatus, Msg}) when room_not_found =:= ResStatus; - muc_server_not_found =:= ResStatus -> - {error, not_found, Msg}; -format_err_result({ResStatus, Msg}) when already_exist =:= ResStatus; - not_allowed =:= ResStatus; - not_room_member =:= ResStatus -> - {error, denied, Msg}; -format_err_result({_, Reason}) -> {error, internal, Reason}. From b5ec345f79f12ddd13b32daaa0188e7956382974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:27:50 +0200 Subject: [PATCH 041/117] Remove the obsolete mongoose_commands --- src/ejabberd_app.erl | 1 - src/mongoose_commands.erl | 693 -------------------------------------- src/mongoose_hooks.erl | 18 - 3 files changed, 712 deletions(-) delete mode 100644 src/mongoose_commands.erl diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index c41cadbb63c..dfea351e799 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -51,7 +51,6 @@ start(normal, _Args) -> ejabberd_node_id:start(), ejabberd_ctl:init(), ejabberd_commands:init(), - mongoose_commands:init(), mongoose_graphql_commands:start(), mongoose_config:start(), mongoose_router:start(), diff --git a/src/mongoose_commands.erl b/src/mongoose_commands.erl deleted file mode 100644 index 2b5098ded25..00000000000 --- a/src/mongoose_commands.erl +++ /dev/null @@ -1,693 +0,0 @@ -%% @doc Mongoose version of command management -%% The following is loosely based on old ejabberd_commands implementation, -%% with some modification related to type check, permission control -%% and the likes. -%% -%% This is a central registry of commands which can be exposed via -%% REST, XMPP as ad-hoc commands or in any other way. Any module can -%% define its commands and register them here. -%% -%% ==== Usage ==== -%% -%% A module defines a list of commands; a command definition is a proplist -%% with the following keys: -%% name :: atom() -%% name of the command by which we refer to it -%% category :: binary() -%% this defines what group the command belongs to, like user, chatroom etc -%% desc :: binary() -%% long description -%% module :: module() -%% module to call -%% function :: atom() -%% function to call -%% action :: command_action() -%% so that the HTTP side can decide which verb to require -%% args = [] :: [argspec()] -%% Type spec - see below; this is both for introspection and type check on call. Args spec is more -%% limited then return, it has to be a list of named arguments, like [{id, integer}, {msg, binary}] -%% security_policy = [atom()] (optional) -%% permissions required to run this command, defaults to [admin] -%% result :: argspec() -%% Type spec of return value of the function to call; execute/3 eventually returns {ok, result} -%% identifiers :: [atom()] (optional, required in 'update' commands) -%% optargs :: [{atom(), type(), term()] (optional args with type and default value. -%% Then a command is called, it fills missing arguments with values from here. -%% We have then two arities: arity of a command, which is only its required arguments, -%% and arity of the function to be called, which is required args + optional args. -%% -%% You can ignore return value of the target func by specifying return value as {result, ok}. The -%% execute/3 will then always return just 'ok' (or error). -%% -%% If action is 'update' then it MUST specify which args are to be used as identifiers of object -%% to update. It has no effect on how the engine does its job, but may be used by client code -%% to enforce proper structure of request. (this is bad programming practice but we didn't have -%% a better idea, we had to solve it for REST API) -%% -%% Commands are registered here upon the module's initialisation -%% (the module has to explicitly call mongoose_commands:register_commands/1 -%% func, it doesn't happen automagically), also should be unregistered when module -%% terminates. -%% -%% Commands are executed by calling mongoose_commands:execute/3 method. This -%% can return: -%% {ok, Result} -%% {error, denied, Msg} if user has no permission -%% {error, not_implemented, Msg} -%% {error, type_error, Msg} if either arguments or return value does not match -%% {error, internal, Msg} if an exception was caught -%% -%% ==== Type check ==== -%% -%% A command's definition includes specification of it arguments; when -%% it is called, arguments are check for compatibility. Examples of specs -%% and compliant arguments: -%% -%% ``` -%% a single type spec -%% integer 2 -%% binary <<"zzz">> -%% atom brrr -%% a list of arbitrary length, of a given type -%% [integer] [] -%% [integer] [1] -%% [integer] [1, 2, 3, 4] -%% a list of anything -%% [] -%% a named argument (name is only for clarity) -%% {msg, binary} <<"zzz">> -%% a tuple of args -%% {integer, binary, float} {1, <<"2">>, 3.0} -%% a tuple of named args -%% {{x, integer}, {y, binary}} {1, <<"bbb">>} -%% ''' -%% -%% Arg specification is used at call-time for control, and also for introspection -%% (see list/1, list/2, mongoose_commands:get_command/2 and args/1) -%% -%% Return value is also specified, and this is a bit tricky: command definition -%% contains spec of return value - what the target func returns should comply to it. -%% The registry, namely execute/3, returns a tuple {ok, ValueReturnedByTheFunction} -%% If return value is defined as 'ok' then whatever target func returns is ignored. -%% This is mostly to make a distinction between 'create' actions which actually create something -%% and return its identifier and those 'lame creators' which cause some action to be done and -%% something written to dbase (exemplum: sending a message), but there is no accessible resource. -%% -%% Called function may also return a tuple {error, term()}, this is returned by the registry -%% as {error, internal, Msg::binary()} -%% -%% ==== Permission control ==== -%% -%% First argument to every function exported from this module is always -%% a user. If you call it from trusted place, you can pass 'admin' here and -%% the whole permission check is skipped. Otherwise, pass #jid record. -%% -%% A command MAY define a security policy to be applied -%% (and this is not yet designed) -%% If it doesn't, then the command is accessible to 'admin' calls only. -%% - --module(mongoose_commands). --author("bartlomiej.gorny@erlang-solutions.com"). --include("mongoose.hrl"). --include("jlib.hrl"). - --record(mongoose_command, - { - %% name of the command by which we refer to it - name :: atom(), - %% groups commands related to the same functionality (user managment, messages/archive) - category :: binary(), - %% optimal subcategory - subcategory = undefined :: undefined | binary(), - %% long description - desc :: binary(), - %% module to call - module :: module(), - %% function to call - function :: atom(), - %% so that the HTTP side can decide which verb to require - action :: action(), - %% this is both for introspection and type check on call - args = [] :: [argspec()], - %% arg which has a default value and is optional - optargs = [] :: [optargspec()], - %% internal use - caller_pos :: integer(), - %% resource identifiers, a subset of args - identifiers = [] :: [atom()], - %% permissions required to run this command - security_policy = [admin] :: security(), - %% what the called func should return; if ok then return of called function is ignored - result :: argspec()|ok - }). - --opaque t() :: #mongoose_command{}. --type caller() :: admin | binary() | user. --type action() :: create | read | update | delete. %% just basic CRUD; sending a mesage is 'create' - --type typedef() :: [typedef_basic()] | typedef_basic(). - --type typedef_basic() :: boolean | integer | binary | float. %% most basic primitives, string is a binary - --type argspec() :: typedef() - | {atom(), typedef()} %% a named argument - | {argspec()} % a tuple of a few args (can be of any size) - | [typedef()]. % a list, but one element - --type optargspec() :: {atom(), typedef(), term()}. % name, type, default value - --type security() :: [admin | user]. %% later acl option will be added - --type errortype() :: denied | not_implemented | bad_request | type_error | not_found | internal. --type errorreason() :: term(). - --type args() :: [{atom(), term()}] | map(). --type failure() :: {error, errortype(), errorreason()}. --type success() :: ok | {ok, term()}. - --export_type([t/0]). --export_type([caller/0]). --export_type([action/0]). --export_type([argspec/0]). --export_type([optargspec/0]). --export_type([errortype/0]). --export_type([errorreason/0]). --export_type([failure/0]). - --type command_properties() :: [{atom(), term()}]. - -%%%% API - --export([check_type/3]). --export([init/0]). - --export([register/1, - unregister/1, - list/1, - list/2, - list/3, - list/4, - register_commands/1, - unregister_commands/1, - new/1, - get_command/2, - execute/3, - name/1, - category/1, - subcategory/1, - desc/1, - args/1, - optargs/1, - arity/1, - func_arity/1, - identifiers/1, - action/1, - result/1 - ]). - --ignore_xref([check_type/3, func_arity/1, get_command/2, list/3, new/1, - register_commands/1, unregister_commands/1, result/1]). - -%% @doc creates new command object based on provided proplist --spec new(command_properties()) -> t(). -new(Props) -> - Fields = record_info(fields, mongoose_command), - Lst = check_command([], Props, Fields), - RLst = lists:reverse(Lst), - Cmd = list_to_tuple([mongoose_command|RLst]), - check_identifiers(Cmd#mongoose_command.action, - Cmd#mongoose_command.identifiers, - Cmd#mongoose_command.args), - % store position of caller in args (if present) - Cmd#mongoose_command{caller_pos = locate_caller(Cmd#mongoose_command.args)}. - - -%% @doc Register mongoose commands. This can be run by any module that wants its commands exposed. --spec register([command_properties()]) -> ok. -register(Cmds) -> - Commands = [new(C) || C <- Cmds], - register_commands(Commands). - -%% @doc Unregister mongoose commands. Should be run when module is unloaded. --spec unregister([command_properties()]) -> ok. -unregister(Cmds) -> - Commands = [new(C) || C <- Cmds], - unregister_commands(Commands). - -%% @doc List commands, available for this user. --spec list(caller()) -> [t()]. -list(U) -> - list(U, any, any, any). - -%% @doc List commands, available for this user, filtered by category. --spec list(caller(), binary() | any) -> [t()]. -list(U, C) -> - list(U, C, any, any). - -%% @doc List commands, available for this user, filtered by category and action. --spec list(caller(), binary() | any, atom()) -> [t()]. -list(U, Category, Action) -> - list(U, Category, Action, any). - -%% @doc List commands, available for this user, filtered by category, action and subcategory --spec list(caller(), binary() | any, atom(), binary() | any | undefined) -> [t()]. -list(U, Category, Action, SubCategory) -> - CL = command_list(Category, Action, SubCategory), - lists:filter(fun(C) -> is_available_for(U, C) end, CL). - -%% @doc Get command definition, if allowed for this user. --spec get_command(caller(), atom()) -> t(). -get_command(Caller, Name) -> - case ets:lookup(mongoose_commands, Name) of - [C] -> - case is_available_for(Caller, C) of - true -> - C; - false -> - {error, denied, <<"Command not available">>} - end; - [] -> {error, not_implemented, <<"Command not implemented">>} - end. - -%% accessors --spec name(t()) -> atom(). -name(Cmd) -> - Cmd#mongoose_command.name. - --spec category(t()) -> binary(). -category(Cmd) -> - Cmd#mongoose_command.category. - --spec subcategory(t()) -> binary() | undefined. -subcategory(Cmd) -> - Cmd#mongoose_command.subcategory. - --spec desc(t()) -> binary(). -desc(Cmd) -> - Cmd#mongoose_command.desc. - --spec args(t()) -> term(). -args(Cmd) -> - Cmd#mongoose_command.args. - --spec optargs(t()) -> term(). -optargs(Cmd) -> - Cmd#mongoose_command.optargs. - --spec identifiers(t()) -> [atom()]. -identifiers(Cmd) -> - Cmd#mongoose_command.identifiers. - --spec action(t()) -> action(). -action(Cmd) -> - Cmd#mongoose_command.action. - --spec result(t()) -> term(). -result(Cmd) -> - Cmd#mongoose_command.result. - --spec arity(t()) -> integer(). -arity(Cmd) -> - length(Cmd#mongoose_command.args). - --spec func_arity(t()) -> integer(). -func_arity(Cmd) -> - length(Cmd#mongoose_command.args) + length(Cmd#mongoose_command.optargs). - -%% @doc Command execution. --spec execute(caller(), atom() | t(), args()) -> - success() | failure(). -execute(Caller, Name, Args) when is_atom(Name) -> - case ets:lookup(mongoose_commands, Name) of - [Command] -> execute_command(Caller, Command, Args); - [] -> {error, not_implemented, {command_not_supported, Name, sizeof(Args)}} - end; -execute(Caller, #mongoose_command{name = Name}, Args) -> - execute(Caller, Name, Args). - -init() -> - ets:new(mongoose_commands, [named_table, set, public, - {keypos, #mongoose_command.name}]). - -%%%% end of API --spec register_commands([t()]) -> ok. -register_commands(Commands) -> - lists:foreach( - fun(Command) -> - check_registration(Command), %% may throw - ets:insert_new(mongoose_commands, Command), - mongoose_hooks:register_command(Command), - ok - end, - Commands). - --spec unregister_commands([t()]) -> ok. -unregister_commands(Commands) -> - lists:foreach( - fun(Command) -> - ets:delete_object(mongoose_commands, Command), - mongoose_hooks:unregister_command(Command) - end, - Commands). - --spec execute_command(caller(), atom() | t(), args()) -> - success() | failure(). -execute_command(Caller, Command, Args) -> - try check_and_execute(Caller, Command, Args) of - ignore_result -> - ok; - {error, Type, Reason} -> - {error, Type, Reason}; - {ok, Res} -> - {ok, Res} - catch - % admittedly, not the best style of coding, in Erlang at least. But we have to do plenty - % of various checks, and in absence of something like Elixir's "with" construct we are - % facing a choice between throwing stuff or using some more or less tortured syntax - % to chain these checks. - throw:{Type, Reason} -> - {error, Type, Reason}; - Class:Reason:Stacktrace -> - Err = #{what => command_failed, - command_name => Command#mongoose_command.name, - caller => Caller, args => Args, - class => Class, reason => Reason, stacktrace => Stacktrace}, - ?LOG_ERROR(Err), - {error, internal, mongoose_lib:term_to_readable_binary(Err)} - end. - -add_defaults(Args, Opts) when is_map(Args) -> - COpts = [{K, V} || {K, _, V} <- Opts], - Missing = lists:subtract(proplists:get_keys(Opts), maps:keys(Args)), - lists:foldl(fun(K, Ar) -> maps:put(K, proplists:get_value(K, COpts), Ar) end, - Args, Missing). - -% @doc This performs many checks - types, permissions etc, may throw one of many exceptions -%% returns what the func returned or just ok if command spec tells so --spec check_and_execute(caller(), t(), args()) -> success() | failure() | ignore_result. -check_and_execute(Caller, Command, Args) when is_map(Args) -> - Args1 = add_defaults(Args, Command#mongoose_command.optargs), - ArgList = maps_to_list(Args1, Command#mongoose_command.args, Command#mongoose_command.optargs), - check_and_execute(Caller, Command, ArgList); -check_and_execute(Caller, Command, Args) -> - % check permissions - case is_available_for(Caller, Command) of - true -> - ok; - false -> - throw({denied, "Command not available for this user"}) - end, - % check caller (if it is given in args, and the engine is called by a 'real' user, then it - % must match - check_caller(Caller, Command, Args), - % check args - % this is the 'real' spec of command - optional args included - FullSpec = Command#mongoose_command.args - ++ [{K, T} || {K, T, _} <- Command#mongoose_command.optargs], - SpecLen = length(FullSpec), - ALen = length(Args), - case SpecLen =/= ALen of - true -> - type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]); - _ -> ok - end, - [check_type(argument, S, A) || {S, A} <- lists:zip(FullSpec, Args)], - % run command - Res = apply(Command#mongoose_command.module, Command#mongoose_command.function, Args), - case Res of - {error, Type, Reason} -> - {error, Type, Reason}; - _ -> - case Command#mongoose_command.result of - ok -> - ignore_result; - ResSpec -> - check_type(return, ResSpec, Res), - {ok, Res} - end - end. - -check_type(_, ok, _) -> - ok; -check_type(_, A, A) -> - true; -check_type(_, {_Name, boolean}, Value) when is_boolean(Value) -> - true; -check_type(Mode, {Name, boolean}, Value) -> - type_error(Mode, "For ~p expected boolean, got ~p", [Name, Value]); -check_type(_, {_Name, binary}, Value) when is_binary(Value) -> - true; -check_type(Mode, {Name, binary}, Value) -> - type_error(Mode, "For ~p expected binary, got ~p", [Name, Value]); -check_type(_, {_Name, integer}, Value) when is_integer(Value) -> - true; -check_type(Mode, {Name, integer}, Value) -> - type_error(Mode, "For ~p expected integer, got ~p", [Name, Value]); -check_type(Mode, {_Name, [_] = LSpec}, Value) when is_list(Value) -> - check_type(Mode, LSpec, Value); -check_type(Mode, Spec, Value) when is_tuple(Spec) and not is_tuple(Value) -> - type_error(Mode, "~p is not a tuple", [Value]); -check_type(Mode, Spec, Value) when is_tuple(Spec) -> - compare_tuples(Mode, Spec, Value); -check_type(_, [_Spec], []) -> - true; -check_type(Mode, [Spec], [H|T]) -> - check_type(Mode, {none, Spec}, H), - check_type(Mode, [Spec], T); -check_type(_, [], [_|_]) -> - true; -check_type(_, [], []) -> - true; -check_type(Mode, Spec, Value) -> - type_error(Mode, "Catch-all: ~p vs ~p", [Spec, Value]). - -compare_tuples(Mode, Spec, Val) -> - Ssize = tuple_size(Spec), - Vsize = tuple_size(Val), - case Ssize of - Vsize -> - compare_lists(Mode, tuple_to_list(Spec), tuple_to_list(Val)); - _ -> - type_error(Mode, "Tuples of different size: ~p and ~p", [Spec, Val]) - end. - -compare_lists(_, [], []) -> - true; -compare_lists(Mode, [S|Sp], [V|Val]) -> - check_type(Mode, S, V), - compare_lists(Mode, Sp, Val). - -type_error(argument, Fmt, V) -> - throw({type_error, io_lib:format(Fmt, V)}); -type_error(return, Fmt, V) -> - throw({internal, io_lib:format(Fmt, V)}). - -check_identifiers(update, [], _) -> - baddef(identifiers, empty); -check_identifiers(update, Ids, Args) -> - check_identifiers(Ids, Args); -check_identifiers(_, _, _) -> - ok. - -check_identifiers([], _) -> - ok; -check_identifiers([H|T], Args) -> - case proplists:get_value(H, Args) of - undefined -> baddef(H, missing); - _ -> check_identifiers(T, Args) - end. - -check_command(Cmd, _PL, []) -> - Cmd; -check_command(Cmd, PL, [N|Tail]) -> - V = proplists:get_value(N, PL), - Val = check_value(N, V), - check_command([Val|Cmd], PL, Tail). - -check_value(name, V) when is_atom(V) -> - V; -check_value(category, V) when is_binary(V) -> - V; -check_value(subcategory, V) when is_binary(V) -> - V; -check_value(subcategory, undefined) -> - undefined; -check_value(desc, V) when is_binary(V) -> - V; -check_value(module, V) when is_atom(V) -> - V; -check_value(function, V) when is_atom(V) -> - V; -check_value(action, read) -> - read; -check_value(action, send) -> - send; -check_value(action, create) -> - create; -check_value(action, update) -> - update; -check_value(action, delete) -> - delete; -check_value(args, V) when is_list(V) -> - Filtered = [C || {C, _} <- V], - ArgCount = length(V), - case length(Filtered) of - ArgCount -> V; - _ -> baddef(args, V) - end; -check_value(security_policy, undefined) -> - [admin]; -check_value(security_policy, []) -> - baddef(security_policy, empty); -check_value(security_policy, V) when is_list(V) -> - lists:map(fun check_security_policy/1, V), - V; -check_value(result, undefined) -> - baddef(result, undefined); -check_value(result, V) -> - V; -check_value(identifiers, undefined) -> - []; -check_value(identifiers, V) -> - V; -check_value(optargs, undefined) -> - []; -check_value(optargs, V) -> - V; -check_value(caller_pos, _) -> - 0; -check_value(K, V) -> - baddef(K, V). - -%% @doc Known security policies -check_security_policy(user) -> - ok; -check_security_policy(admin) -> - ok; -check_security_policy(Other) -> - baddef(security_policy, Other). - -baddef(K, V) -> - throw({invalid_command_definition, io_lib:format("~p=~p", [K, V])}). - -command_list(Category, Action, SubCategory) -> - Cmds = [C || [C] <- ets:match(mongoose_commands, '$1')], - filter_commands(Category, Action, SubCategory, Cmds). - -filter_commands(any, any, _, Cmds) -> - Cmds; -filter_commands(Cat, any, _, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat]; -filter_commands(any, _, _, _) -> - throw({invalid_filter, ""}); -filter_commands(Cat, Action, any, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat, - C#mongoose_command.action == Action]; -filter_commands(Cat, Action, SubCategory, Cmds) -> - [C || C <- Cmds, C#mongoose_command.category == Cat, - C#mongoose_command.action == Action, - C#mongoose_command.subcategory == SubCategory]. - - -%% @doc make sure the command may be registered -%% it may not if either (a) command of that name is already registered, -%% (b) there is a command in the same category and subcategory with the same action and arity -check_registration(Command) -> - Name = name(Command), - Cat = category(Command), - Act = action(Command), - Arity = arity(Command), - SubCat = subcategory(Command), - case ets:lookup(mongoose_commands, Name) of - [] -> - CatLst = list(admin, Cat), - FCatLst = [C || C <- CatLst, action(C) == Act, - subcategory(C) == SubCat, - arity(C) == Arity], - case FCatLst of - [] -> ok; - [C] -> - baddef("There is a command ~p in category ~p and subcategory ~p, action ~p", - [name(C), Cat, SubCat, Act]) - end; - Other -> - ?LOG_DEBUG(#{what => command_conflict, - text => <<"This command is already defined">>, - command_name => Name, registered => Other}), - ok - end. - -mapget(K, Map) -> - try maps:get(K, Map) of - V -> V - catch - error:{badkey, K} -> - type_error(argument, "Missing argument: ~p", [K]); - error:bad_key -> - type_error(argument, "Missing argument: ~p", [K]) - end. - -maps_to_list(Map, Args, Optargs) -> - SpecLen = length(Args) + length(Optargs), - ALen = maps:size(Map), - case SpecLen of - ALen -> ok; - _ -> type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]) - end, - [mapget(K, Map) || {K, _} <- Args] ++ [mapget(K, Map) || {K, _, _} <- Optargs]. - -%% @doc Main entry point for permission control - is this command available for this user -is_available_for(User, C) when is_binary(User) -> - is_available_for(jid:from_binary(User), C); -is_available_for(admin, _C) -> - true; -is_available_for(Jid, #mongoose_command{security_policy = Policies}) -> - apply_policies(Policies, Jid). - -%% @doc Check all security policies defined in the command - passes if any of them returns true -apply_policies([], _) -> - false; -apply_policies([P|Policies], Jid) -> - case apply_policy(P, Jid) of - true -> - true; - false -> - apply_policies(Policies, Jid) - end. - -%% @doc This is the only policy we know so far, but there will be others (like roles/acl control) -apply_policy(user, _) -> - true; -apply_policy(_, _) -> - false. - -locate_caller(L) -> - locate_caller(1, L). - -locate_caller(_I, []) -> - 0; -locate_caller(I, [{caller, _}|_]) -> - I; -locate_caller(I, [_|T]) -> - locate_caller(I + 1, T). - -check_caller(admin, _Command, _Args) -> - ok; -check_caller(_Caller, #mongoose_command{caller_pos = 0}, _Args) -> - % no caller in args - ok; -check_caller(Caller, #mongoose_command{caller_pos = CallerPos}, Args) -> - % check that server and user match (we don't care about resource) - ACaller = lists:nth(CallerPos, Args), - CallerJid = jid:from_binary(Caller), - ACallerJid = jid:from_binary(ACaller), - case jid:are_bare_equal(CallerJid, ACallerJid) of - true -> - ok; - _ -> - throw({denied, "Caller ids do not match"}) - end. - -sizeof(#{} = M) -> maps:size(M); -sizeof([_|_] = L) -> length(L). diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index c60646178f9..7b831a77887 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -24,7 +24,6 @@ packet_to_component/3, presence_probe_hook/5, push_notifications/4, - register_command/1, register_subhost/2, register_user/3, remove_user/3, @@ -34,7 +33,6 @@ set_vcard/3, unacknowledged_message/2, filter_unacknowledged_messages/3, - unregister_command/1, unregister_subhost/1, user_available_hook/2, user_ping_response/5, @@ -334,14 +332,6 @@ push_notifications(HostType, Acc, NotificationForms, Options) -> ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), run_hook_for_host_type(push_notifications, HostType, Acc, ParamsWithLegacyArgs). -%%% @doc The `register_command' hook is called when a command -%%% is registered in `mongoose_commands'. --spec register_command(Command) -> Result when - Command :: mongoose_commands:t(), - Result :: drop. -register_command(Command) -> - run_global_hook(register_command, Command, []). - %%% @doc The `register_subhost' hook is called when a component %%% is registered for ejabberd_router or a subdomain is added to mongoose_subdomain_core. -spec register_subhost(LDomain, IsHidden) -> Result when @@ -433,14 +423,6 @@ unacknowledged_message(Acc, JID) -> filter_unacknowledged_messages(HostType, Jid, Buffer) -> run_fold(filter_unacknowledged_messages, HostType, Buffer, #{jid => Jid}). -%%% @doc The `unregister_command' hook is called when a command -%%% is unregistered from `mongoose_commands'. --spec unregister_command(Command) -> Result when - Command :: mongoose_commands:t(), - Result :: drop. -unregister_command(Command) -> - run_global_hook(unregister_command, Command, []). - %%% @doc The `unregister_subhost' hook is called when a component %%% is unregistered from ejabberd_router or a subdomain is removed from mongoose_subdomain_core. -spec unregister_subhost(LDomain) -> Result when From a59ac20dce320e4120617cb81fd2e1713cb28baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:34:29 +0200 Subject: [PATCH 042/117] Remove and reorganize REST API helpers - Remove unused helpers from mongoose_api_common - Move error handling to mongoose_admin_api, which is specific to the Admin API. - Rework error loggiong, the log level is quite verbose (warning/error), but still lower than before (always error). Anyway, it applies only to the Admin API. --- src/mongoose_admin_api/mongoose_admin_api.erl | 49 ++- src/mongoose_api_common.erl | 345 +----------------- 2 files changed, 43 insertions(+), 351 deletions(-) diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index bf089545643..36e103c7eca 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -18,12 +18,14 @@ resource_created/4, respond/3]). +-include("mongoose.hrl"). -include("mongoose_config_spec.hrl"). -type handler_options() :: #{path := string(), username => binary(), password => binary(), atom() => any()}. -type req() :: cowboy_req:req(). -type state() :: map(). +-type error_type() :: bad_request | denied | not_found | internal. %% mongoose_http_handler callbacks @@ -102,28 +104,37 @@ authorize(#{username := Username, password := Password}, AuthDetails) -> authorize(#{}, _) -> true. % no credentials required --spec parse_body(req()) -> jiffy:json_value(). +-spec parse_body(req()) -> #{atom() => jiffy:json_value()}. parse_body(Req) -> try - {Params, _} = mongoose_api_common:parse_request_body(Req), - maps:from_list(Params) - catch _:_ -> + {ok, Body, _Req2} = cowboy_req:read_body(Req), + {DecodedBody} = jiffy:decode(Body), + maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- DecodedBody]) + catch Class:Reason:Stacktrace -> + ?LOG_WARNING(#{what => parse_body_failed, + class => Class, reason => Reason, stacktrace => Stacktrace}), throw_error(bad_request, <<"Invalid request body">>) end. --spec parse_qs(req()) -> #{atom() => binary()}. +-spec parse_qs(req()) -> #{atom() => binary() | true}. parse_qs(Req) -> - maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]). + try + maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]) + catch Class:Reason:Stacktrace -> + ?LOG_WARNING(#{what => parse_qs_failed, + class => Class, reason => Reason, stacktrace => Stacktrace}), + throw_error(bad_request, <<"Invalid query string">>) + end. -spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result. try_handle_request(Req, State, F) -> try F(Req, State) catch throw:#{error_type := ErrorType, message := Msg} -> - mongoose_api_common:error_response(ErrorType, Msg, Req, State) + error_response(ErrorType, Msg, Req, State) end. --spec throw_error(atom(), iodata()) -> no_return(). +-spec throw_error(error_type(), iodata()) -> no_return(). throw_error(ErrorType, Msg) -> throw(#{error_type => ErrorType, message => Msg}). @@ -140,3 +151,25 @@ respond(Req, State, Response) -> Req2 = cowboy_req:set_resp_body(jiffy:encode(Response), Req), Req3 = cowboy_req:reply(200, Req2), {stop, Req3, State}. + +-spec error_response(error_type(), iodata(), req(), state()) -> {stop, req(), state()}. +error_response(ErrorType, Message, Req, State) -> + BinMessage = iolist_to_binary(Message), + ?LOG(log_level(ErrorType), #{what => mongoose_admin_api_error_response, + error_type => ErrorType, + message => BinMessage, + req => Req}), + Req1 = cowboy_req:reply(error_code(ErrorType), #{}, BinMessage, Req), + {stop, Req1, State}. + +-spec error_code(error_type()) -> non_neg_integer(). +error_code(bad_request) -> 400; +error_code(denied) -> 403; +error_code(not_found) -> 404; +error_code(internal) -> 500. + +-spec log_level(error_type()) -> logger:level(). +log_level(bad_request) -> warning; +log_level(denied) -> warning; +log_level(not_found) -> warning; +log_level(internal) -> error. diff --git a/src/mongoose_api_common.erl b/src/mongoose_api_common.erl index 7bb1fc39fe8..ee9bd22b408 100644 --- a/src/mongoose_api_common.erl +++ b/src/mongoose_api_common.erl @@ -3,358 +3,17 @@ %%% @copyright (C) 2016, Erlang Solutions Ltd. %%% Created : 20. Jul 2016 10:16 %%%------------------------------------------------------------------- - -%% @doc MongooseIM REST API backend -%% -%% This module handles the client HTTP REST requests, then respectively convert -%% them to Commands from mongoose_commands and execute with `admin` privileges. -%% It supports responses with appropriate HTTP Status codes returned to the -%% client. -%% This module implements behaviour introduced in ejabberd_cowboy which is -%% %% built on top of the cowboy library. -%% The method supported: GET, POST, PUT, DELETE. Only JSON format. -%% The library "jiffy" used to serialize and deserialized JSON data. -%% -%% REQUESTS -%% -%% The module is based on mongoose_commands registry. -%% The root http path for a command is build based on the "category" field. -%% %% It's always used as path a prefix. -%% The commands are translated to HTTP API in the following manner: -%% -%% command of action "read" will be called by GET request -%% command of action "create" will be called by POST request -%% command of action "update" will be called by PUT request -%% command of action "delete" will be called by DELETE request -%% -%% The args of the command will be filled with the values provided in path -%% %% bindings or body parameters, depending of the method type: -%% - for command of action "read" or "delete" all the args are pulled from the -%% path bindings. The path should be constructed of pairs "/arg_name/arg_value" -%% so that it could match the {arg_name, type} %% pattern in the command -%% registry. E.g having the record of category "users" and args: -%% [{username, binary}, {domain, binary}] we will have to make following GET -%% request %% path: http://domain:port/api/users/username/Joe/domain/localhost -%% and the command will be called with arguments "Joe" and "localhost" -%% -%% - for command of action "create" or "update" args are pulled from the body -%% JSON, except those that are on the "identifiers" list of the command. Those -%% go to the path bindings. -%% E.g having the record of category "animals", action "update" and args: -%% [{species, binary}, {name, binary}, {age, integer}] -%% and identifiers: -%% [species, name] -%% we can set the age for our elephant Ed in the PUT request: -%% path: http://domain:port/api/species/elephant/name/Ed -%% body: {"age" : "10"} -%% and then the command will be called with arguments ["elephant", "Ed" and 10]. -%% -%% RESPONSES -%% -%% The API supports some of the http status code like 200, 201, 400, 404 etc -%% depending on the return value of the command execution and arguments checks. -%% Additionally, when the command's action is "create" and it returns a value, -%% it is concatenated to the path and return to the client in header "location" -%% with response code 201 so that it could represent now a new created resource. -%% If error occured while executing the command, the appropriate reason is -%% returned in response body. +%%% @doc Utilities for the REST API -module(mongoose_api_common). -author("ludwikbukowski"). --include("mongoose_api.hrl"). --include("mongoose.hrl"). %% API --export([create_admin_url_path/1, - action_to_method/1, - method_to_action/1, - parse_request_body/1, - get_allowed_methods/1, - process_request/4, - reload_dispatches/1, - get_auth_details/1, +-export([get_auth_details/1, is_known_auth_method/1, - error_response/4, make_unauthorized_response/2, check_password/2]). --ignore_xref([reload_dispatches/1]). - -%% @doc Reload all ejabberd_cowboy listeners. -%% When a command is registered or unregistered, the routing paths that -%% cowboy stores as a "dispatch" must be refreshed. -%% Read more http://ninenines.eu/docs/en/cowboy/1.0/guide/routing/ -reload_dispatches(drop) -> - drop; -reload_dispatches(_Command) -> - Listeners = supervisor:which_children(mongoose_listener_sup), - CowboyListeners = [Child || {_Id, Child, _Type, [ejabberd_cowboy]} <- Listeners], - [ejabberd_cowboy:reload_dispatch(Child) || Child <- CowboyListeners], - drop. - - --spec create_admin_url_path(mongoose_commands:t()) -> ejabberd_cowboy:path(). -create_admin_url_path(Command) -> - iolist_to_binary(create_admin_url_path_iodata(Command)). - -create_admin_url_path_iodata(Command) -> - ["/", mongoose_commands:category(Command), - maybe_add_bindings(Command, admin), maybe_add_subcategory(Command)]. - --spec process_request(Method :: method(), - Command :: mongoose_commands:t(), - Req :: cowboy_req:req() | {list(), cowboy_req:req()}, - State :: http_api_state()) -> - {any(), cowboy_req:req(), http_api_state()}. -process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity} = State) - when ((Method == <<"POST">>) or (Method == <<"PUT">>)) -> - {Params, Req2} = Req, - QVals = cowboy_req:parse_qs(Req2), - QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals], - Params2 = Binds ++ Params ++ QV ++ maybe_add_caller(Entity), - handle_request(Method, Command, Params2, Req2, State); -process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity}=State) - when ((Method == <<"GET">>) or (Method == <<"DELETE">>)) -> - QVals = cowboy_req:parse_qs(Req), - QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals], - BindsAndVars = Binds ++ QV ++ maybe_add_caller(Entity), - handle_request(Method, Command, BindsAndVars, Req, State). - --spec handle_request(Method :: method(), - Command :: mongoose_commands:t(), - Args :: args_applied(), - Req :: cowboy_req:req(), - State :: http_api_state()) -> - {any(), cowboy_req:req(), http_api_state()}. -handle_request(Method, Command, Args, Req, #http_api_state{entity = Entity} = State) -> - case check_and_extract_args(mongoose_commands:args(Command), - mongoose_commands:optargs(Command), Args) of - {error, Type, Reason} -> - handle_result(Method, {error, Type, Reason}, Req, State); - ConvertedArgs -> - handle_result(Method, - execute_command(ConvertedArgs, Command, Entity), - Req, State) - end. - --type correct_result() :: mongoose_commands:success(). --type error_result() :: mongoose_commands:failure(). - --spec handle_result(Method, Result, Req, State) -> Return when - Method :: method() | no_call, - Result :: correct_result() | error_result(), - Req :: cowboy_req:req(), - State :: http_api_state(), - Return :: {any(), cowboy_req:req(), http_api_state()}. -handle_result(<<"DELETE">>, ok, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"DELETE">>, {ok, Res}, Req, State) -> - Req2 = cowboy_req:set_resp_body(jiffy:encode(Res), Req), - Req3 = cowboy_req:reply(200, Req2), - {jiffy:encode(Res), Req3, State}; -handle_result(Verb, ok, Req, State) -> - handle_result(Verb, {ok, nocontent}, Req, State); -handle_result(<<"GET">>, {ok, Result}, Req, State) -> - {jiffy:encode(Result), Req, State}; -handle_result(<<"POST">>, {ok, nocontent}, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"POST">>, {ok, Res}, Req, State) -> - Path = iolist_to_binary(cowboy_req:uri(Req)), - Req2 = cowboy_req:set_resp_body(Res, Req), - Req3 = maybe_add_location_header(Res, binary_to_list(Path), Req2), - {stop, Req3, State}; -handle_result(<<"PUT">>, {ok, nocontent}, Req, State) -> - Req2 = cowboy_req:reply(204, Req), - {stop, Req2, State}; -handle_result(<<"PUT">>, {ok, Res}, Req, State) -> - Req2 = cowboy_req:set_resp_body(Res, Req), - Req3 = cowboy_req:reply(201, Req2), - {stop, Req3, State}; -handle_result(_, {error, Error, Reason}, Req, State) -> - error_response(Error, Reason, Req, State); -handle_result(no_call, _, Req, State) -> - error_response(not_implemented, <<>>, Req, State). - - --spec parse_request_body(any()) -> {args_applied(), cowboy_req:req()} | {error, any()}. -parse_request_body(Req) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), - {Data} = jiffy:decode(Body), - try - Params = create_params_proplist(Data), - {Params, Req2} - catch - Class:Reason:StackTrace -> - ?LOG_ERROR(#{what => parse_request_body_failed, class => Class, - reason => Reason, stacktrace => StackTrace}), - {error, Reason} - end. - -%% @doc Checks if the arguments are correct. Return the arguments that can be applied to the -%% execution of command. --spec check_and_extract_args(arg_spec_list(), optarg_spec_list(), args_applied()) -> - map() | {error, atom(), any()}. -check_and_extract_args(ReqArgs, OptArgs, RequestArgList) -> - try - AllArgs = ReqArgs ++ [{N, T} || {N, T, _} <- OptArgs], - AllArgVals = [{N, T, proplists:get_value(N, RequestArgList)} || {N, T} <- AllArgs], - ConvArgs = [{N, convert_arg(T, V)} || {N, T, V} <- AllArgVals, V =/= undefined], - maps:from_list(ConvArgs) - catch - Class:Reason:StackTrace -> - ?LOG_ERROR(#{what => check_and_extract_args_failed, class => Class, - reason => Reason, stacktrace => StackTrace}), - {error, bad_request, Reason} - end. - --spec execute_command(mongoose_commands:args(), - mongoose_commands:t(), - mongoose_commands:caller()) -> - correct_result() | error_result(). -execute_command(ArgMap, Command, Entity) -> - mongoose_commands:execute(Entity, mongoose_commands:name(Command), ArgMap). - --spec maybe_add_caller(admin | binary()) -> list() | list({caller, binary()}). -maybe_add_caller(admin) -> - []; -maybe_add_caller(JID) -> - [{caller, JID}]. - --spec maybe_add_location_header(binary() | list(), list(), any()) - -> cowboy_req:req(). -maybe_add_location_header(Result, ResourcePath, Req) when is_binary(Result) -> - add_location_header(binary_to_list(Result), ResourcePath, Req); -maybe_add_location_header(Result, ResourcePath, Req) when is_list(Result) -> - add_location_header(Result, ResourcePath, Req); -maybe_add_location_header(_, _Path, Req) -> - cowboy_req:reply(204, #{}, Req). - -add_location_header(Result, ResourcePath, Req) -> - Path = [ResourcePath, "/", Result], - Headers = #{<<"location">> => Path}, - cowboy_req:reply(201, Headers, Req). - --spec convert_arg(atom(), any()) -> boolean() | integer() | float() | binary() | string() | {error, bad_type}. -convert_arg(binary, Binary) when is_binary(Binary) -> - Binary; -convert_arg(boolean, Value) when is_boolean(Value) -> - Value; -convert_arg(integer, Binary) when is_binary(Binary) -> - binary_to_integer(Binary); -convert_arg(integer, Integer) when is_integer(Integer) -> - Integer; -convert_arg(float, Binary) when is_binary(Binary) -> - binary_to_float(Binary); -convert_arg(float, Float) when is_float(Float) -> - Float; -convert_arg([Type], List) when is_list(List) -> - [ convert_arg(Type, Item) || Item <- List ]; -convert_arg(_, _Binary) -> - throw({error, bad_type}). - --spec create_params_proplist(list({binary(), binary()})) -> args_applied(). -create_params_proplist(ArgList) -> - lists:sort([{to_atom(Arg), Value} || {Arg, Value} <- ArgList]). - -%% @doc Returns list of allowed methods. --spec get_allowed_methods(admin | user) -> list(method()). -get_allowed_methods(Entity) -> - Commands = mongoose_commands:list(Entity), - [action_to_method(mongoose_commands:action(Command)) || Command <- Commands]. - --spec maybe_add_bindings(mongoose_commands:t(), admin|user) -> iolist(). -maybe_add_bindings(Command, Entity) -> - Action = mongoose_commands:action(Command), - Args = mongoose_commands:args(Command), - BindAndBody = both_bind_and_body(Action), - case BindAndBody of - true -> - Ids = mongoose_commands:identifiers(Command), - Bindings = [El || {Key, _Value} = El <- Args, true =:= proplists:is_defined(Key, Ids)], - add_bindings(Bindings, Entity); - false -> - add_bindings(Args, Entity) - end. - -maybe_add_subcategory(Command) -> - SubCategory = mongoose_commands:subcategory(Command), - case SubCategory of - undefined -> - []; - _ -> - ["/", SubCategory] - end. - --spec both_bind_and_body(mongoose_commands:action()) -> boolean(). -both_bind_and_body(update) -> - true; -both_bind_and_body(create) -> - true; -both_bind_and_body(read) -> - false; -both_bind_and_body(delete) -> - false. - -add_bindings(Args, Entity) -> - [add_bind(A, Entity) || A <- Args]. - -%% skip "caller" arg for frontend command -add_bind({ArgName, _}, _Entity) -> - lists:flatten(["/:", atom_to_list(ArgName)]); -add_bind(Other, _) -> - throw({error, bad_arg_spec, Other}). - --spec to_atom(binary() | atom()) -> atom(). -to_atom(Bin) when is_binary(Bin) -> - erlang:binary_to_existing_atom(Bin, utf8); -to_atom(Atom) when is_atom(Atom) -> - Atom. - -%%-------------------------------------------------------------------- -%% HTTP utils -%%-------------------------------------------------------------------- -%%-spec error_response(mongoose_commands:errortype(), any(), http_api_state()) -> -%% {stop, any(), http_api_state()}. -%%error_response(ErrorType, Req, State) -> -%% error_response(ErrorType, <<>>, Req, State). - --spec error_response(mongoose_commands:errortype(), mongoose_commands:errorreason(), cowboy_req:req(), http_api_state()) -> - {stop, cowboy_req:req(), http_api_state()}. -error_response(ErrorType, Reason, Req, State) -> - BinReason = case Reason of - B when is_binary(B) -> B; - L when is_list(L) -> list_to_binary(L); - Other -> list_to_binary(io_lib:format("~p", [Other])) - end, - ?LOG_ERROR(#{what => rest_common_error, - error_type => ErrorType, reason => Reason, req => Req}), - Req1 = cowboy_req:reply(error_code(ErrorType), #{}, BinReason, Req), - {stop, Req1, State}. - -%% HTTP status codes -error_code(denied) -> 403; -error_code(not_implemented) -> 501; -error_code(bad_request) -> 400; -error_code(type_error) -> 400; -error_code(not_found) -> 404; -error_code(internal) -> 500; -error_code(Other) -> - ?WARNING_MSG("Unknown error identifier \"~p\". See mongoose_commands:errortype() for allowed values.", [Other]), - 500. - -action_to_method(read) -> <<"GET">>; -action_to_method(update) -> <<"PUT">>; -action_to_method(delete) -> <<"DELETE">>; -action_to_method(create) -> <<"POST">>; -action_to_method(_) -> undefined. - -method_to_action(<<"GET">>) -> read; -method_to_action(<<"POST">>) -> create; -method_to_action(<<"PUT">>) -> update; -method_to_action(<<"DELETE">>) -> delete. - %%-------------------------------------------------------------------- %% Authorization %%-------------------------------------------------------------------- From 80fcc62dcab2f48a01ab5b65f0acbb66c2359c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:39:04 +0200 Subject: [PATCH 043/117] Clean up unused functions 'reload_dispatch' is kept as a shell utility --- src/ejabberd_cowboy.erl | 2 +- src/mongoose_lib.erl | 4 ---- src/mongoose_stanza_helper.erl | 28 ---------------------------- 3 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/ejabberd_cowboy.erl b/src/ejabberd_cowboy.erl index a390b52f77e..6f810362300 100644 --- a/src/ejabberd_cowboy.erl +++ b/src/ejabberd_cowboy.erl @@ -40,7 +40,7 @@ -export([ref/1, reload_dispatch/1]). -export([start_cowboy/4, start_cowboy/2, stop_cowboy/1]). --ignore_xref([behaviour_info/1, process/1, ref/1, socket_type/0, start_cowboy/2, +-ignore_xref([behaviour_info/1, process/1, ref/1, reload_dispatch/1, socket_type/0, start_cowboy/2, start_cowboy/4, start_link/1, start_listener/2, start_listener/1, stop_cowboy/1]). -include("mongoose.hrl"). diff --git a/src/mongoose_lib.erl b/src/mongoose_lib.erl index 9625d4c8411..e3340a06700 100644 --- a/src/mongoose_lib.erl +++ b/src/mongoose_lib.erl @@ -10,7 +10,6 @@ -export([wait_until/2, wait_until/3]). -export([parse_ip_netmask/1]). --export([term_to_readable_binary/1]). -export([get_message_type/1, does_local_user_exist/3]). %% Private, just for warning @@ -156,9 +155,6 @@ parse_ip_netmask(IPStr, MaskStr) -> error end. -term_to_readable_binary(X) -> - iolist_to_binary(io_lib:format("~0p", [X])). - %% ------------------------------------------------------------------ %% does_local_user_exist %% ------------------------------------------------------------------ diff --git a/src/mongoose_stanza_helper.erl b/src/mongoose_stanza_helper.erl index 85aaa1212b3..ea1025c4f1c 100644 --- a/src/mongoose_stanza_helper.erl +++ b/src/mongoose_stanza_helper.erl @@ -1,7 +1,6 @@ -module(mongoose_stanza_helper). -export([build_message/3]). -export([build_message_with_headline/3]). --export([parse_from_to/2]). -export([get_last_messages/4]). -export([route/4]). @@ -37,33 +36,6 @@ maybe_cdata_elem(Name, Text) when is_binary(Text) -> cdata_elem(Name, Text) when is_binary(Name), is_binary(Text) -> #xmlel{name = Name, children = [#xmlcdata{content = Text}]}. --spec parse_from_to(jid:jid() | binary() | undefined, jid:jid() | binary() | undefined) -> - {ok, jid:jid(), jid:jid()} | {error, missing} | {error, type_error, string()}. -parse_from_to(F, T) when F == undefined; T == undefined -> - {error, missing}; -parse_from_to(F, T) -> - case parse_jid_list([F, T]) of - {ok, [Fjid, Tjid]} -> {ok, Fjid, Tjid}; - E -> E - end. - --spec parse_jid_list(BinJids :: [binary()]) -> {ok, [jid:jid()]} | {error, type_error, string()}. -parse_jid_list([_ | _] = BinJids) -> - Jids = lists:map(fun parse_jid/1, BinJids), - case [Msg || {error, Msg} <- Jids] of - [] -> {ok, Jids}; - Errors -> {error, type_error, lists:join("; ", Errors)} - end. - --spec parse_jid(binary() | jid:jid()) -> jid:jid() | {error, string()}. -parse_jid(#jid{} = Jid) -> - Jid; -parse_jid(Jid) when is_binary(Jid) -> - case jid:from_binary(Jid) of - error -> {error, io_lib:format("Invalid jid: ~p", [Jid])}; - B -> B - end. - -spec get_last_messages(Caller :: jid:jid(), Limit :: non_neg_integer(), With :: null | jid:jid(), From 27e048627dbdaceaee92e9afa17a205c9658a481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 15 Sep 2022 14:40:03 +0200 Subject: [PATCH 044/117] Test the handling of invalid query string --- big_tests/tests/rest_SUITE.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/big_tests/tests/rest_SUITE.erl b/big_tests/tests/rest_SUITE.erl index ce7c0de79e7..a0fb556e0d7 100644 --- a/big_tests/tests/rest_SUITE.erl +++ b/big_tests/tests/rest_SUITE.erl @@ -71,6 +71,7 @@ blank_auth_testcases() -> test_cases() -> [non_existent_command_returns404, existent_command_with_missing_arguments_returns404, + invalid_query_string, invalid_request_body, user_can_be_registered_and_removed, user_registration_errors, @@ -166,6 +167,13 @@ non_existent_command_returns404(_C) -> existent_command_with_missing_arguments_returns404(_C) -> {?NOT_FOUND, _} = gett(admin, <<"/contacts/">>). +invalid_query_string(Config) -> + Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]), + AliceJid = escalus_users:get_jid(Config1, alice), + BobJid = escalus_users:get_jid(Config1, bob), + {?BAD_REQUEST, <<"Invalid query string">>} = + gett(admin, <<"/messages/", AliceJid/binary, "/", BobJid/binary, "?kukurydza">>). + invalid_request_body(_Config) -> {?BAD_REQUEST, <<"Invalid request body">>} = post(admin, path("users"), <<"kukurydza">>). From 3ce9205a3489989eb8fc0bb2cccb6a46201d7aa3 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Thu, 15 Sep 2022 17:16:32 +0200 Subject: [PATCH 045/117] Fix cache affiliations key in the accumulator In 884ca87045137c3d27baec5b1a2de42057443fa9, with the good intention of introducing an API to manage the accumulated affiliations, I didn't realise the old keying was used elsewhere: in the muc cache. So we've been missing using the cache recently. This is fixed now. --- src/muc_light/mod_muc_light.erl | 2 ++ src/muc_light/mod_muc_light_cache.erl | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/muc_light/mod_muc_light.erl b/src/muc_light/mod_muc_light.erl index 4e9109b562d..29af66aab78 100644 --- a/src/muc_light/mod_muc_light.erl +++ b/src/muc_light/mod_muc_light.erl @@ -63,6 +63,8 @@ %% for mod_muc_light_codec_legacy -export([subdomain_pattern/1]). +-export([get_room_affs_from_acc/2, set_room_affs_from_acc/3]). + %% For tests -export([default_schema/0, force_clear_from_ct/1]). diff --git a/src/muc_light/mod_muc_light_cache.erl b/src/muc_light/mod_muc_light_cache.erl index 13d9891037d..29eb4f33173 100644 --- a/src/muc_light/mod_muc_light_cache.erl +++ b/src/muc_light/mod_muc_light_cache.erl @@ -3,7 +3,6 @@ -include("mongoose_config_spec.hrl"). -behaviour(gen_mod). --define(FRONTEND, mod_muc_light). %% gen_mod callbacks -export([start/2, stop/1, config_spec/0, supported_features/0]). @@ -55,12 +54,12 @@ hooks(HostType) -> -spec pre_acc_room_affiliations(mongoose_acc:t(), jid:jid()) -> mongoose_acc:t() | {stop, mongoose_acc:t()}. pre_acc_room_affiliations(Acc, RoomJid) -> - case mongoose_acc:get(?FRONTEND, affiliations, {error, not_exists}, Acc) of + case mod_muc_light:get_room_affs_from_acc(Acc, RoomJid) of {error, _} -> HostType = mongoose_acc:host_type(Acc), case mongoose_user_cache:get_entry(HostType, ?MODULE, RoomJid) of #{affs := Res} -> - mongoose_acc:set(?FRONTEND, affiliations, Res, Acc); + mod_muc_light:set_room_affs_from_acc(Acc, RoomJid, Res); _ -> Acc end; @@ -70,7 +69,7 @@ pre_acc_room_affiliations(Acc, RoomJid) -> -spec post_acc_room_affiliations(mongoose_acc:t(), jid:jid()) -> mongoose_acc:t(). post_acc_room_affiliations(Acc, RoomJid) -> - case mongoose_acc:get(?FRONTEND, affiliations, {error, not_exists}, Acc) of + case mod_muc_light:get_room_affs_from_acc(Acc, RoomJid) of {error, _} -> Acc; Res -> From 0a2f070d3fadafbaf96a049bb669d511843d57fe Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Thu, 15 Sep 2022 09:33:30 +0200 Subject: [PATCH 046/117] Fixing CR issues, deleting .DS_STORE --- big_tests/tests/graphql_server_SUITE.erl | 18 ++++------- priv/graphql/schemas/admin/server.gql | 10 +++--- src/mongoose_server_api.erl | 39 +++++++++++++---------- test/.DS_Store | Bin 14340 -> 0 bytes 4 files changed, 35 insertions(+), 32 deletions(-) delete mode 100644 test/.DS_Store diff --git a/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl index 253b61549fe..6745b79c76a 100644 --- a/big_tests/tests/graphql_server_SUITE.erl +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -89,15 +89,12 @@ init_per_testcase(CaseName, Config) -> end_per_testcase(CaseName, Config) when CaseName == join_successful orelse CaseName == leave_unsuccessful orelse CaseName == join_twice -> - Node2 = mim2(), - remove_node_from_cluster(Node2, Config), + remove_node_from_cluster(mim2(), Config), escalus:end_per_testcase(CaseName, Config); end_per_testcase(CaseName, Config) when CaseName == remove_alive_from_cluster orelse CaseName == remove_dead_from_cluster -> - Node3 = mim3(), - Node2 = mim2(), - remove_node_from_cluster(Node3, Config), - remove_node_from_cluster(Node2, Config), + remove_node_from_cluster(mim3(), Config), + remove_node_from_cluster(mim2(), Config), escalus:end_per_testcase(CaseName, Config); end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). @@ -110,7 +107,7 @@ set_and_get_loglevel_test(Config) -> LogLevels = all_log_levels(), lists:foreach(fun(LogLevel) -> Value = get_ok_value([data, server, setLoglevel], set_loglevel(LogLevel, Config)), - ?assertEqual(<<"Log level successfully set">>, Value), + ?assertEqual(<<"Log level successfully set.">>, Value), Value1 = get_ok_value([data, server, getLoglevel], get_loglevel(Config)), ?assertEqual(LogLevel, Value1) end, LogLevels), @@ -191,15 +188,14 @@ remove_alive_from_cluster(Config) -> remove_node_test(Config) -> #{node := NodeName} = mim3(), Value = get_ok_value([data, server, removeNode], remove_node(NodeName, Config)), - ?assertEqual(<<"Node removed from the Mnesia schema">>, Value). + ?assertEqual(<<"MongooseIM node removed from the Mnesia schema">>, Value). stop_node_test(Config) -> - #{node := Node1Name} = mim(), #{node := Node3Nodename} = mim3(), get_ok_value([data, server, stop], stop_node(Node3Nodename, Config)), - timer:sleep(1000), Timeout = timer:seconds(3), - {badrpc, nodedown} = rpc:call(Node3Nodename, mongoose_cluster, join, [Node1Name], Timeout), + F = fun() -> rpc:call(Node3Nodename, application, which_applications, [], Timeout) end, + mongoose_helper:wait_until(F, {badrpc, nodedown}, #{sleep_time => 1000, name => stop_node}), distributed_helper:start_node(Node3Nodename, Config). %----------------------------------------------------------------------- diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index 180ddcadf84..c3c430a5b4c 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -17,25 +17,25 @@ type ServerAdminQuery @protected{ Allow admin to manage the node """ type ServerAdminMutation @protected{ - "Join the node to a cluster. Call it on the joining node" + "Join the MongooseIM node to a cluster. Call it on the joining node" joinCluster(node: String!): String @protected(type: GLOBAL) "Leave a cluster. Call it on the node that is going to leave" leaveCluster: String @protected(type: GLOBAL) - "Remove node from the cluster. Call it from the member of the cluster" + "Remove a MongooseIM node from the cluster. Call it from the member of the cluster" removeFromCluster(node: String!): String @protected(type: GLOBAL) - "Restart mongooseim node" + "Restart MongooseIM node" restart: String @protected(type: GLOBAL) - "Stop mongooseim node" + "Stop MongooseIM node" stop: String @protected(type: GLOBAL) "Remove a MongooseIM node from Mnesia clustering config" removeNode(node: String!): String @protected(type: GLOBAL) - "Set Node's loglevel" + "Set MongooseIM Node's loglevel" setLoglevel(level: LogLevel!): String @protected(type: GLOBAL) } diff --git a/src/mongoose_server_api.erl b/src/mongoose_server_api.erl index d2c5d900fde..6a5637102d7 100644 --- a/src/mongoose_server_api.erl +++ b/src/mongoose_server_api.erl @@ -22,21 +22,21 @@ graphql_get_loglevel() -> set_loglevel(Level) -> case mongoose_logs:set_global_loglevel(Level) of ok -> - {ok, "Log level successfully set"}; + {ok, "Log level successfully set."}; {error, _} -> - {invalid_level, io_lib:format("Log level ~p does not exist", [Level])} + {invalid_level, io_lib:format("Log level ~p does not exist.", [Level])} end. -spec status() -> {'mongooseim_not_running', string()} | {'ok', string()}. status() -> {InternalStatus, ProvidedStatus} = init:get_status(), - String1 = io_lib:format("The node ~p is ~p. Status: ~p", + String1 = io_lib:format("The node ~p is ~p. Status: ~p.", [node(), InternalStatus, ProvidedStatus]), case lists:keysearch(mongooseim, 1, application:which_applications()) of false -> - {mongooseim_not_running, String1 ++ "mongooseim is not running in that node."}; + {mongooseim_not_running, String1 ++ " MongooseIM is not running in that node."}; {value, {_, _, Version}} -> - {ok, String1 ++ io_lib:format("mongooseim ~s is running in that node", [Version])} + {ok, String1 ++ io_lib:format(" MongooseIM ~s is running in that node.", [Version])} end. -spec get_cookie() -> string(). @@ -50,7 +50,8 @@ join_cluster(NodeString) -> NodeList = mnesia:system_info(db_nodes), case lists:member(NodeAtom, NodeList) of true -> - String = io_lib:format("The node ~s has already joined the cluster~n", [NodeString]), + String = io_lib:format( + "The MongooseIM node ~s has already joined the cluster~n.", [NodeString]), {already_joined, String}; _ -> do_join_cluster(NodeAtom) @@ -59,12 +60,13 @@ join_cluster(NodeString) -> do_join_cluster(Node) -> try mongoose_cluster:join(Node) of ok -> - String = io_lib:format("You have successfully joined the node" - ++ " ~p to the cluster with node member ~p~n", [node(), Node]), + String = io_lib:format("You have successfully added the MongooseIM node" + ++ " ~p to the cluster with node member ~p~n.", [node(), Node]), {ok, String} catch error:pang -> - String = io_lib:format("Timeout while attempting to connect to node ~s~n", [Node]), + String = io_lib:format( + "Timeout while attempting to connect to a MongooseIM node ~s~n", [Node]), {pang, String}; error:{cant_get_storage_type, {T, E, R}} -> String = @@ -80,7 +82,7 @@ leave_cluster() -> ThisNode = node(), case NodeList of [ThisNode] -> - String = io_lib:format("The node ~p is not in the cluster~n", [node()]), + String = io_lib:format("The MongooseIM node ~p is not in the cluster~n", [node()]), {not_in_cluster, String}; _ -> do_leave_cluster() @@ -89,7 +91,8 @@ leave_cluster() -> do_leave_cluster() -> try mongoose_cluster:leave() of ok -> - String = io_lib:format("The node ~p has successfully left the cluster~n", [node()]), + String = io_lib:format( + "The MongooseIM node ~p has successfully left the cluster~n", [node()]), {ok, String} catch E:R -> @@ -114,11 +117,13 @@ remove_dead_node(DeadNode) -> try mongoose_cluster:remove_from_cluster(DeadNode) of ok -> String = - io_lib:format("The dead node ~p has been removed from the cluster~n", [DeadNode]), + io_lib:format( + "The dead MongooseIM node ~p has been removed from the cluster~n", [DeadNode]), {ok, String} catch error:{node_is_alive, DeadNode} -> - String = io_lib:format("The node ~p is alive but shoud not be.~n", [DeadNode]), + String = io_lib:format( + "The MongooseIM node ~p is alive but shoud not be.~n", [DeadNode]), {node_is_alive, String}; error:{del_table_copy_schema, R} -> String = io_lib:format("Cannot delete table schema~n. Reason: ~p", [R]), @@ -129,10 +134,12 @@ remove_rpc_alive_node(AliveNode) -> case rpc:call(AliveNode, mongoose_cluster, leave, []) of {badrpc, Reason} -> String = - io_lib:format("Cannot remove the node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), + io_lib:format( + "Cannot remove the MongooseIM node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), {rpc_error, String}; ok -> - String = io_lib:format("The node ~p has been removed from the cluster~n", [AliveNode]), + String = io_lib:format( + "The MongooseIM node ~p has been removed from the cluster~n", [AliveNode]), {ok, String}; Unknown -> String = io_lib:format("Unknown error: ~p~n", [Unknown]), @@ -152,4 +159,4 @@ restart() -> -spec remove_node(string()) -> {ok, string()}. remove_node(Node) -> mnesia:del_table_copy(schema, list_to_atom(Node)), - {ok, "Node removed from the Mnesia schema"}. + {ok, "MongooseIM node removed from the Mnesia schema"}. diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index 1757ddf835dc026210e9c5f2c597cffb8061c937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14340 zcmeHNO>Y}T7@kej;2eVAlw8`Ir-Dlc5Asut$kR3EFm>YlojPNROpomYo6N}a~{ z?3GC%lRzecOahq%{*Mx1?!_yY@$-~C! zUN}dIF4v05xSouMdK6O72q{KQ8YEkcK$GKok+)-9PsT$%YH}55aup;q2gwd4kl8_3 zl-pIXuE*>%lRzecaS6PQOH0^yN2-QhbXoPWbLZ|ol+m@TejhvWAB|o0g(1>5cIA<( z0Fk@%8us>G^aVN?D}KB63AhcQ#OMr^6#E05qb!_v)tVbOxH!V^@{r#h;IMnKgN{Rt zb7w~(_+|v_Xzh{O!n=jFhCm}G$1cA6;Qld!TNv`#2TBDv5qch93ZK-KLr2mrF?DoN z@Iti>oCs*-unbu?F;;P9C;8optf2krONRE(r699AQwM%1`f_8UPh(-8O~s6Ww*`*3 z-Rq~GEW~s!(-CR8W!i(?fubG#eauR~+fKS#KUdnVW;b44{WX|6HhuiWYq{L1+^LyU zpKmsz{$^Z`d)(SBm@ZUdQ>3wVMYACvz)rU3>5D zgPpy{`(GVA`A$cWAj!gNZX}|7kMA!e;N8tm*os25_K@cH5gJGA*2p7rBa$O zRz@;p8T}nl#>6d{TCXT?FoCZtxvar)QZvpqQ+5=h#JokimI-nC>Tk(YU5lqZtiex)jeFg6()?Gq^i68}rua0~D;=Crl4u)!PD0%*!b<=tAoi@cBYECRXsLVFhVY=)*S{&^?>s z(>}o3o$Q^;{SnXD|H77U&qVsqnJ&;_%G_Ld9`mYKSg^4QKWU0F!yfHdgfvB0F} zV%US;%I>Ngun(8KAax6KUJ5*I&Gb1p0_?y-SbJXN z<8?ZO7?`P?g7D@>n-m!75++_=@cQClK4G86`MY791<2Uq1-ADfwP=oYzH_TzE zA}tf|tT*0-6^iMbh|4L6RdwCRy(NH{SQoson8bCXy*r`er^}hRiz&FyR4%q*qNM4f z0Zy0}Jc1bm&(s%&ZqKK1#eIq1<I(>}AhlR9| z?ABOLY3aE!;8zyL@(^S22bu0cd+33BwXo*LupC|`n*7Dk8l~~Sr$s~|T;3Z2Sn7+Mvdjp`s1YkaESH$rE$KZu~<#v3?qj(tQQ9OL% zqj>KO?&FBx)|2s2k3x#;LW=PpdI&JJ!yqO2tdY*M{r|t@Nt0~<|CH{8M`qdUc)tDr EUmXSCE&u=k From 723d58b17eb9f6f07d6530d640e5cb3d5eb396f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Thu, 15 Sep 2022 13:06:19 +0200 Subject: [PATCH 047/117] Refactored hook handlers in ejabberd_s2s module --- src/ejabberd_s2s.erl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index cdd9dc41a6f..bb3a62086fe 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -49,7 +49,7 @@ ]). %% Hooks callbacks --export([node_cleanup/2]). +-export([node_cleanup/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -59,7 +59,7 @@ -export([get_info_s2s_connections/1]). -ignore_xref([dirty_get_connections/0, get_info_s2s_connections/1, have_connection/1, - incoming_s2s_number/0, node_cleanup/2, outgoing_s2s_number/0, start_link/0]). + incoming_s2s_number/0, outgoing_s2s_number/0, start_link/0]). -include("mongoose.hrl"). -include("jlib.hrl"). @@ -163,7 +163,8 @@ dirty_get_connections() -> %% Hooks callbacks %%==================================================================== -node_cleanup(Acc, Node) -> +-spec node_cleanup(map(), map(), map()) -> {ok, map()}. +node_cleanup(Acc, #{node := Node}, _) -> F = fun() -> Es = mnesia:select( s2s, @@ -175,7 +176,7 @@ node_cleanup(Acc, Node) -> end, Es) end, Res = mnesia:async_dirty(F), - maps:put(?MODULE, Res, Acc). + {ok, maps:put(?MODULE, Res, Acc)}. -spec key(mongooseim:host_type(), {jid:lserver(), jid:lserver()}, binary()) -> binary(). @@ -205,7 +206,7 @@ init([]) -> mnesia:add_table_copy(s2s_shared, node(), ram_copies), {atomic, ok} = set_shared_secret(), ejabberd_commands:register_commands(commands()), - ejabberd_hooks:add(node_cleanup, global, ?MODULE, node_cleanup, 50), + gen_hook:add_handlers(hooks()), {ok, #state{}}. %%-------------------------------------------------------------------- @@ -250,7 +251,7 @@ handle_info(Msg, State) -> %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> - ejabberd_hooks:delete(node_cleanup, global, ?MODULE, node_cleanup, 50), + gen_hook:delete_handlers(hooks()), ejabberd_commands:unregister_commands(commands()), ok. @@ -264,6 +265,9 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- +-spec hooks() -> [gen_hook:hook_tuple()]. +hooks() -> + [{node_cleanup, global, fun ?MODULE:node_cleanup/3, #{}, 50}]. -spec do_route(From :: jid:jid(), To :: jid:jid(), From 82aa71fcfa04165436e67f7c3e7fad0ab392ad55 Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Fri, 16 Sep 2022 16:36:45 +0200 Subject: [PATCH 048/117] Domain admin tests focused on existing domains --- big_tests/tests/domain_helper.erl | 20 +- big_tests/tests/graphql_account_SUITE.erl | 217 ++++---- big_tests/tests/graphql_domain_SUITE.erl | 22 +- big_tests/tests/graphql_gdpr_SUITE.erl | 30 +- big_tests/tests/graphql_http_upload_SUITE.erl | 8 +- big_tests/tests/graphql_inbox_SUITE.erl | 64 +-- big_tests/tests/graphql_last_SUITE.erl | 40 +- big_tests/tests/graphql_muc_SUITE.erl | 190 +++++-- big_tests/tests/graphql_muc_light_SUITE.erl | 504 ++++++++++-------- big_tests/tests/graphql_offline_SUITE.erl | 23 +- big_tests/tests/graphql_private_SUITE.erl | 30 +- big_tests/tests/graphql_session_SUITE.erl | 73 ++- big_tests/tests/graphql_stanza_SUITE.erl | 30 +- big_tests/tests/graphql_stats_SUITE.erl | 9 +- big_tests/tests/graphql_token_SUITE.erl | 31 +- big_tests/tests/graphql_vcard_SUITE.erl | 6 +- big_tests/tests/muc_helper.erl | 4 +- priv/graphql/schemas/admin/session.gql | 2 +- 18 files changed, 796 insertions(+), 507 deletions(-) diff --git a/big_tests/tests/domain_helper.erl b/big_tests/tests/domain_helper.erl index 74e056379af..ffae07f3dcc 100644 --- a/big_tests/tests/domain_helper.erl +++ b/big_tests/tests/domain_helper.erl @@ -1,24 +1,6 @@ -module(domain_helper). --export([insert_configured_domains/0, - delete_configured_domains/0, - insert_domain/3, - insert_persistent_domain/3, - delete_domain/2, - delete_persistent_domain/3, - set_domain_password/3, - delete_domain_password/2, - make_metrics_prefix/1, - host_types/0, - host_types/1, - host_type/0, - host_type/1, - domain_to_host_type/2, - domain/0, - secondary_domain/0, - domain/1, - secondary_host_type/0, - secondary_host_type/1]). +-compile([export_all, nowarn_export_all]). -import(distributed_helper, [get_or_fail/1, rpc/4, mim/0]). diff --git a/big_tests/tests/graphql_account_SUITE.erl b/big_tests/tests/graphql_account_SUITE.erl index 3b55953855a..f5f56e323b7 100644 --- a/big_tests/tests/graphql_account_SUITE.erl +++ b/big_tests/tests/graphql_account_SUITE.erl @@ -32,11 +32,16 @@ user_account_tests() -> admin_account_tests() -> [admin_list_users, + admin_list_users_unknown_domain, admin_count_users, + admin_count_users_unknown_domain, admin_check_password, + admin_check_password_non_exisiting_user, admin_check_password_hash, + admin_check_password_hash_non_existing_user, admin_check_plain_password_hash, admin_check_user, + admin_check_non_existing_user, admin_register_user, admin_register_random_user, admin_remove_non_existing_user, @@ -45,20 +50,27 @@ admin_account_tests() -> admin_change_user_password]. domain_admin_tests() -> - [domain_admin_list_users, - domain_admin_count_users, - domain_admin_check_password, - domain_admin_check_password_hash, - domain_admin_check_plain_password_hash, - domain_admin_check_user, + [admin_list_users, + domain_admin_list_users_no_permission, + admin_count_users, + domain_admin_count_users_no_permission, + admin_check_password, + domain_admin_check_password_no_permission, + admin_check_password_hash, + domain_admin_check_password_hash_no_permission, + domain_admin_check_plain_password_hash_no_permission, + admin_check_user, + domain_admin_check_user_no_permission, admin_register_user, domain_admin_register_user_no_permission, admin_register_random_user, - domain_admin_remove_non_existing_user, + domain_admin_register_random_user_no_permission, admin_remove_existing_user, domain_admin_remove_user_no_permission, - domain_admin_ban_user, - domain_admin_change_user_password]. + admin_ban_user, + domain_admin_ban_user_no_permission, + admin_change_user_password, + domain_admin_change_user_password_no_permission]. init_per_suite(Config) -> Config1 = [{ctl_auth_mods, mongoose_helper:auth_modules()} | Config], @@ -118,7 +130,7 @@ init_per_testcase(admin_check_plain_password_hash = C, Config) -> Config2 = escalus:create_users(Config1, escalus:get_users([carol])), escalus:init_per_testcase(C, Config2) end; -init_per_testcase(domain_admin_check_plain_password_hash = C, Config) -> +init_per_testcase(domain_admin_check_plain_password_hash_no_permission = C, Config) -> {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config), case lists:member(ejabberd_auth_ldap, AuthMods) of true -> @@ -143,7 +155,7 @@ end_per_testcase(admin_register_user = C, Config) -> end_per_testcase(admin_check_plain_password_hash, Config) -> mongoose_helper:restore_config(Config), escalus:delete_users(Config, escalus:get_users([carol])); -end_per_testcase(domain_admin_check_plain_password_hash, Config) -> +end_per_testcase(domain_admin_check_plain_password_hash_no_permission, Config) -> mongoose_helper:restore_config(Config), escalus:delete_users(Config, escalus:get_users([carol, alice_bis])); end_per_testcase(domain_admin_register_user = C, Config) -> @@ -178,10 +190,6 @@ user_change_password_story(Config, Alice) -> ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"Password changed">>)). admin_list_users(Config) -> - % An unknown domain - Resp = list_users(<<"unknown-domain">>, Config), - ?assertEqual([], get_ok_value([data, account, listUsers], Resp)), - % A domain with users Domain = domain_helper:domain(), Username = jid:nameprep(escalus_users:get_username(Config, alice)), JID = <>, @@ -189,15 +197,20 @@ admin_list_users(Config) -> Users = get_ok_value([data, account, listUsers], Resp2), ?assert(lists:member(JID, Users)). +admin_list_users_unknown_domain(Config) -> + Resp = list_users(<<"unknown-domain">>, Config), + ?assertEqual([], get_ok_value([data, account, listUsers], Resp)). + admin_count_users(Config) -> - % An unknown domain - Resp = count_users(<<"unknown-domain">>, Config), - ?assertEqual(0, get_ok_value([data, account, countUsers], Resp)), % A domain with at least one user Domain = domain_helper:domain(), Resp2 = count_users(Domain, Config), ?assert(0 < get_ok_value([data, account, countUsers], Resp2)). +admin_count_users_unknown_domain(Config) -> + Resp = count_users(<<"unknown-domain">>, Config), + ?assertEqual(0, get_ok_value([data, account, countUsers], Resp)). + admin_check_password(Config) -> Password = lists:last(escalus_users:get_usp(Config, alice)), BinJID = escalus_users:get_jid(Config, alice), @@ -207,10 +220,12 @@ admin_check_password(Config) -> ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), % An incorrect password Resp2 = check_password(BinJID, <<"incorrect_pw">>, Config), - ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)), - % A non-existing user - Resp3 = check_password(?NOT_EXISTING_JID, Password, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp3), <<"not exist">>)). + ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)). + +admin_check_password_non_exisiting_user(Config) -> + Password = lists:last(escalus_users:get_usp(Config, alice)), + Resp = check_password(?NOT_EXISTING_JID, Password, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"not exist">>)). admin_check_password_hash(Config) -> UserSCRAM = escalus_users:get_jid(Config, alice), @@ -218,8 +233,11 @@ admin_check_password_hash(Config) -> Method = <<"md5">>, % SCRAM password user Resp1 = check_password_hash(UserSCRAM, EmptyHash, Method, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)), - % A non-existing user + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)). + +admin_check_password_hash_non_existing_user(Config) -> + EmptyHash = list_to_binary(get_md5(<<>>)), + Method = <<"md5">>, Resp2 = check_password_hash(?NOT_EXISTING_JID, EmptyHash, Method, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"not exist">>)). @@ -243,12 +261,13 @@ admin_check_plain_password_hash(Config) -> admin_check_user(Config) -> BinJID = escalus_users:get_jid(Config, alice), Path = [data, account, checkUser], - % An existing user - Resp1 = check_user(BinJID, Config), - ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), - % A non-existing user - Resp2 = check_user(?NOT_EXISTING_JID, Config), - ?assertMatch(#{<<"exist">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)). + Resp = check_user(BinJID, Config), + ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp)). + +admin_check_non_existing_user(Config) -> + Path = [data, account, checkUser], + Resp = check_user(?NOT_EXISTING_JID, Config), + ?assertMatch(#{<<"exist">> := false, <<"message">> := _}, get_ok_value(Path, Resp)). admin_register_user(Config) -> Password = <<"my_password">>, @@ -289,100 +308,88 @@ admin_remove_existing_user(Config) -> admin_ban_user(Config) -> Path = [data, account, banUser, message], Reason = <<"annoying">>, - % Ban not existing user - Resp1 = ban_user(?NOT_EXISTING_JID, Reason, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)), % Ban an existing user escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> BinJID = escalus_client:full_jid(Alice), - Resp2 = ban_user(BinJID, Reason, Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"successfully banned">>)) + Resp1 = ban_user(BinJID, Reason, Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp1), <<"successfully banned">>)) end). +admin_ban_non_existing_user(Config) -> + Reason = <<"annoying">>, + Resp = ban_user(?NOT_EXISTING_JID, Reason, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"not allowed">>)). + admin_change_user_password(Config) -> Path = [data, account, changeUserPassword, message], NewPassword = <<"new password">>, - % Change password of not existing user - Resp1 = change_user_password(?NOT_EXISTING_JID, NewPassword, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)), - % Set an empty password - Resp2 = change_user_password(?NOT_EXISTING_JID, <<>>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"Empty password">>)), - % Change password of an existing user escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> BinJID = escalus_client:full_jid(Alice), - Resp3 = change_user_password(BinJID, NewPassword, Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) + % Set an empty password + Resp1 = change_user_password(BinJID, <<>>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"Empty password">>)), + % Set non-empty password + Resp2 = change_user_password(BinJID, NewPassword, Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"Password changed">>)) end). -domain_admin_list_users(Config) -> +admin_change_non_exisisting_user_password(Config) -> + NewPassword = <<"new password">>, + Resp = change_user_password(?NOT_EXISTING_JID, NewPassword, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"not allowed">>)). + +domain_admin_list_users_no_permission(Config) -> % An unknown domain - Resp = list_users(<<"unknown-domain">>, Config), - get_unauthorized(Resp), - % A domain with users - Domain = domain_helper:domain(), - Username = jid:nameprep(escalus_users:get_username(Config, alice)), - JID = <>, - Resp2 = list_users(Domain, Config), - Users = get_ok_value([data, account, listUsers], Resp2), - ?assert(lists:member(JID, Users)). + Resp1 = list_users(<<"unknown-domain">>, Config), + get_unauthorized(Resp1), + % An external domain + Resp2 = list_users(domain_helper:secondary_domain(), Config), + get_unauthorized(Resp2). -domain_admin_count_users(Config) -> +domain_admin_count_users_no_permission(Config) -> % An unknown domain - Resp = count_users(<<"unknown-domain">>, Config), - get_unauthorized(Resp), - % A domain with at least one user - Domain = domain_helper:domain(), - Resp2 = count_users(Domain, Config), - ?assert(0 < get_ok_value([data, account, countUsers], Resp2)). + Resp1 = count_users(<<"unknown-domain">>, Config), + get_unauthorized(Resp1), + % An external domain + Resp2 = count_users(domain_helper:secondary_domain(), Config), + get_unauthorized(Resp2). -domain_admin_check_password(Config) -> +domain_admin_check_password_no_permission(Config) -> Password = lists:last(escalus_users:get_usp(Config, alice)), - BinJID = escalus_users:get_jid(Config, alice), - Path = [data, account, checkPassword], - % A correct password - Resp1 = check_password(BinJID, Password, Config), - ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), - % An incorrect password - Resp2 = check_password(BinJID, <<"incorrect_pw">>, Config), - ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)), - % An external domain PasswordOutside = lists:last(escalus_users:get_usp(Config, alice_bis)), BinOutsideJID = escalus_users:get_jid(Config, alice_bis), + % An external domain user Resp3 = check_password(BinOutsideJID, PasswordOutside, Config), get_unauthorized(Resp3), % A non-existing user Resp4 = check_password(?NOT_EXISTING_JID, Password, Config), - get_unauthorized(Resp4). + get_unauthorized(Resp4). -domain_admin_check_password_hash(Config) -> - UserSCRAM = escalus_users:get_jid(Config, alice), +domain_admin_check_password_hash_no_permission(Config) -> ExternalUserSCRAM = escalus_users:get_jid(Config, alice_bis), EmptyHash = list_to_binary(get_md5(<<>>)), Method = <<"md5">>, - % SCRAM password user - Resp1 = check_password_hash(UserSCRAM, EmptyHash, Method, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)), % An external domain user - Resp2 = check_password_hash(ExternalUserSCRAM, EmptyHash, Method, Config), + Resp1 = check_password_hash(ExternalUserSCRAM, EmptyHash, Method, Config), + get_unauthorized(Resp1), + % A non-existing user + Resp2 = check_password_hash(?NOT_EXISTING_JID, EmptyHash, Method, Config), get_unauthorized(Resp2). -domain_admin_check_plain_password_hash(Config) -> +domain_admin_check_plain_password_hash_no_permission(Config) -> Method = <<"md5">>, ExternalUserJID = escalus_users:get_jid(Config, alice_bis), ExternalPassword = lists:last(escalus_users:get_usp(Config, alice_bis)), ExternalHash = list_to_binary(get_md5(ExternalPassword)), get_unauthorized(check_password_hash(ExternalUserJID, ExternalHash, Method, Config)). -domain_admin_check_user(Config) -> - BinJID = escalus_users:get_jid(Config, alice), +domain_admin_check_user_no_permission(Config) -> ExternalBinJID = escalus_users:get_jid(Config, alice_bis), - Path = [data, account, checkUser], - % An existing user - Resp1 = check_user(BinJID, Config), - ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), % An external domain user - Resp2 = check_user(ExternalBinJID, Config), + Resp1 = check_user(ExternalBinJID, Config), + get_unauthorized(Resp1), + % A non-existing user + Resp2 = check_user(?NOT_EXISTING_JID, Config), get_unauthorized(Resp2). domain_admin_register_user_no_permission(Config) -> @@ -390,45 +397,41 @@ domain_admin_register_user_no_permission(Config) -> Domain = <<"unknown-domain">>, get_unauthorized(register_user(Domain, external_user, Password, Config)). -domain_admin_remove_non_existing_user(Config) -> - get_unauthorized(remove_user(?NOT_EXISTING_JID, Config)). +domain_admin_register_random_user_no_permission(Config) -> + Password = <<"my_password">>, + Domain = domain_helper:secondary_domain(), + Resp = register_random_user(Domain, Password, Config), + get_unauthorized(Resp). domain_admin_remove_user_no_permission(Config) -> + get_unauthorized(remove_user(?NOT_EXISTING_JID, Config)), escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> BinJID = escalus_client:full_jid(AliceBis), get_unauthorized(remove_user(BinJID, Config)) end). -domain_admin_ban_user(Config) -> - Path = [data, account, banUser, message], +domain_admin_ban_user_no_permission(Config) -> Reason = <<"annoying">>, % Ban not existing user Resp1 = ban_user(?NOT_EXISTING_JID, Reason, Config), get_unauthorized(Resp1), - % Ban an existing user - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - BinJID = escalus_client:full_jid(Alice), + % Ban an external domain user + escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> + BinJID = escalus_client:full_jid(AliceBis), Resp2 = ban_user(BinJID, Reason, Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"successfully banned">>)) + get_unauthorized(Resp2) end). -domain_admin_change_user_password(Config) -> - Path = [data, account, changeUserPassword, message], +domain_admin_change_user_password_no_permission(Config) -> NewPassword = <<"new password">>, % Change password of not existing user Resp1 = change_user_password(?NOT_EXISTING_JID, NewPassword, Config), get_unauthorized(Resp1), - % Set an empty password - escalus:fresh_story(Config, [{alice, 1}], fun(Alice1) -> - BinJID = escalus_client:full_jid(Alice1), - Resp2 = change_user_password(BinJID, <<>>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"Empty password">>)) - end), - % Change password of an existing user - escalus:fresh_story(Config, [{alice, 1}], fun(Alice2) -> - BinJID = escalus_client:full_jid(Alice2), - Resp3 = change_user_password(BinJID, NewPassword, Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) + % Change external domain user password + escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> + BinJID = escalus_client:full_jid(AliceBis), + Resp2 = change_user_password(BinJID, NewPassword, Config), + get_unauthorized(Resp2) end). %% Helpers diff --git a/big_tests/tests/graphql_domain_SUITE.erl b/big_tests/tests/graphql_domain_SUITE.erl index dd27283b466..2e29ec739c1 100644 --- a/big_tests/tests/graphql_domain_SUITE.erl +++ b/big_tests/tests/graphql_domain_SUITE.erl @@ -210,28 +210,36 @@ domain_admin_set_domain_password(Config) -> ?assertNotEqual(nomatch, binary:match(ParsedResult, <<"successfully">>)). domain_admin_create_domain_no_permission(Config) -> - get_unauthorized(add_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). + get_unauthorized(add_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)), + get_unauthorized(add_domain(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). domain_admin_disable_domain_no_permission(Config) -> - get_unauthorized(disable_domain(?EXAMPLE_DOMAIN, Config)). + get_unauthorized(disable_domain(?EXAMPLE_DOMAIN, Config)), + get_unauthorized(disable_domain(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config)). domain_admin_enable_domain_no_permission(Config) -> - get_unauthorized(enable_domain(?EXAMPLE_DOMAIN, Config)). + get_unauthorized(enable_domain(?EXAMPLE_DOMAIN, Config)), + get_unauthorized(enable_domain(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config)). domain_admin_get_domains_by_host_type_no_permission(Config) -> - get_unauthorized(get_domains_by_host_type(?HOST_TYPE, Config)). + get_unauthorized(get_domains_by_host_type(?HOST_TYPE, Config)), + get_unauthorized(get_domains_by_host_type(domain_helper:host_type(), Config)). domain_admin_get_domain_details_no_permission(Config) -> + get_unauthorized(get_domain_details(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config)), get_unauthorized(get_domain_details(?EXAMPLE_DOMAIN, Config)). domain_admin_set_domain_password_no_permission(Config) -> - get_unauthorized(set_domain_password(<<"externalDomain">>, <<"secret">>, Config)). + get_unauthorized(set_domain_password(?EXAMPLE_DOMAIN, <<"secret">>, Config)), + get_unauthorized(set_domain_password(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, <<"secret">>, Config)). domain_admin_delete_domain_no_permission(Config) -> - get_unauthorized(remove_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). + get_unauthorized(remove_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config)), + get_unauthorized(remove_domain(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). domain_admin_delete_domain_password_no_permission(Config) -> - get_unauthorized(delete_domain_password(domain_helper:domain(), Config)). + get_unauthorized(delete_domain_password(?EXAMPLE_DOMAIN, Config)), + get_unauthorized(delete_domain_password(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config)). %% Commands diff --git a/big_tests/tests/graphql_gdpr_SUITE.erl b/big_tests/tests/graphql_gdpr_SUITE.erl index ae64c5aca47..c862795692f 100644 --- a/big_tests/tests/graphql_gdpr_SUITE.erl +++ b/big_tests/tests/graphql_gdpr_SUITE.erl @@ -19,18 +19,18 @@ all() -> {group, domain_admin_gdpr}]. groups() -> - [{admin_gdpr_http, [], admin_stats_tests()}, - {admin_gdpr_cli, [], admin_stats_tests()}, - {domain_admin_gdpr, [], domain_admin_stats_tests()}]. + [{admin_gdpr_http, [], admin_gdpr_tests()}, + {admin_gdpr_cli, [], admin_gdpr_tests()}, + {domain_admin_gdpr, [], domain_admin_gdpr_tests()}]. -admin_stats_tests() -> - [admin_retrive_user_data, +admin_gdpr_tests() -> + [admin_retrieve_user_data, admin_gdpr_no_user_test]. -domain_admin_stats_tests() -> - [admin_retrive_user_data, +domain_admin_gdpr_tests() -> + [admin_retrieve_user_data, admin_gdpr_no_user_test, - domain_admin_retrive_user_data_no_permission]. + domain_admin_retrieve_user_data_no_permission]. init_per_suite(Config) -> Config1 = escalus:init_per_suite(Config), @@ -59,10 +59,10 @@ end_per_testcase(CaseName, Config) -> % Admin test cases -admin_retrive_user_data(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_retrive_user_data/2). +admin_retrieve_user_data(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_retrieve_user_data/2). -admin_retrive_user_data(Config, Alice) -> +admin_retrieve_user_data(Config, Alice) -> Filename = random_filename(Config), Res = admin_retrieve_personal_data(escalus_client:username(Alice), escalus_client:server(Alice), list_to_binary(Filename), Config), @@ -77,14 +77,14 @@ admin_gdpr_no_user_test(Config) -> Res = admin_retrieve_personal_data(<<"AAAA">>, domain(), <<"AAA">>, Config), ?assertEqual(<<"user_does_not_exist_error">>, get_err_code(Res)). -domain_admin_retrive_user_data_no_permission(Config) -> +domain_admin_retrieve_user_data_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice_bis, 1}], - fun domain_admin_retrive_user_data_no_permission/2). + fun domain_admin_retrieve_user_data_no_permission/2). -domain_admin_retrive_user_data_no_permission(Config, Alice) -> +domain_admin_retrieve_user_data_no_permission(Config, Alice) -> Filename = random_filename(Config), Res = admin_retrieve_personal_data(escalus_client:username(Alice), escalus_client:server(Alice), - list_to_binary(Filename), Config), + list_to_binary(Filename), Config), get_unauthorized(Res). % Helpers diff --git a/big_tests/tests/graphql_http_upload_SUITE.erl b/big_tests/tests/graphql_http_upload_SUITE.erl index fbcc11b4ee9..dd63b0bcf3b 100644 --- a/big_tests/tests/graphql_http_upload_SUITE.erl +++ b/big_tests/tests/graphql_http_upload_SUITE.erl @@ -3,7 +3,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1]). --import(domain_helper, [host_type/0, domain/0]). +-import(domain_helper, [host_type/0, domain/0, secondary_domain/0]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1]). @@ -222,8 +222,10 @@ admin_http_upload_not_configured(Config) -> ?assertEqual(<<"mod_http_upload is not loaded for this host">>, get_err_msg(Result)). domain_admin_get_url_no_permission(Config) -> - Result = admin_get_url(<<"AAAAA">>, <<"test">>, 123, <<"Test">>, 123, Config), - get_unauthorized(Result). + Result1 = admin_get_url(<<"AAAAA">>, <<"test">>, 123, <<"Test">>, 123, Config), + get_unauthorized(Result1), + Result2 = admin_get_url(secondary_domain(), <<"test">>, 123, <<"Test">>, 123, Config), + get_unauthorized(Result2). % Helpers diff --git a/big_tests/tests/graphql_inbox_SUITE.erl b/big_tests/tests/graphql_inbox_SUITE.erl index e575075b506..3ce31cdaeca 100644 --- a/big_tests/tests/graphql_inbox_SUITE.erl +++ b/big_tests/tests/graphql_inbox_SUITE.erl @@ -36,6 +36,7 @@ user_inbox_tests() -> admin_inbox_tests() -> [admin_flush_user_bin, admin_try_flush_nonexistent_user_bin, + admin_try_flush_user_bin_nonexistent_domain, admin_flush_domain_bin, admin_try_flush_nonexistent_domain_bin, admin_flush_global_bin, @@ -44,11 +45,11 @@ admin_inbox_tests() -> domain_admin_inbox_tests() -> [admin_flush_user_bin, - domain_admin_try_flush_nonexistent_user_bin, + admin_try_flush_nonexistent_user_bin, + domain_admin_flush_user_bin_no_permission, admin_flush_domain_bin, domain_admin_try_flush_domain_bin_no_permission, - domain_admin_flush_global_bin_no_permission, - domain_admin_try_flush_nonexistent_host_type_bin]. + domain_admin_flush_global_bin_no_permission]. init_per_suite(Config) -> HostType = domain_helper:host_type(), @@ -98,6 +99,17 @@ admin_flush_user_bin(Config, Alice, Bob, Kate) -> inbox_helper:check_inbox(Bob, [], #{box => bin}), check_aff_msg_in_inbox_bin(Kate, RoomBinJID). +admin_try_flush_nonexistent_user_bin(Config) -> + User = <<"nonexistent-user@", (domain_helper:domain())/binary>>, + Res2 = flush_user_bin(User, Config), + ?assertErrMsg(Res2, <<"does not exist">>), + ?assertErrCode(Res2, user_does_not_exist). + +admin_try_flush_user_bin_nonexistent_domain(Config) -> + Res = flush_user_bin(<<"user@user.com">>, Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, domain_not_found). + admin_flush_domain_bin(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}], fun admin_flush_domain_bin/4). @@ -111,6 +123,11 @@ admin_flush_domain_bin(Config, Alice, AliceBis, Kate) -> inbox_helper:check_inbox(Kate, [], #{box => bin}), check_aff_msg_in_inbox_bin(AliceBis, RoomBinJID). +admin_try_flush_nonexistent_domain_bin(Config) -> + Res = flush_domain_bin(<<"unknown-domain">>, Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, domain_not_found). + admin_flush_global_bin(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}], fun admin_flush_global_bin/4). @@ -137,22 +154,6 @@ admin_flush_global_bin_after_days(Config, Alice, AliceBis, Kate) -> check_aff_msg_in_inbox_bin(AliceBis, RoomBinJID), check_aff_msg_in_inbox_bin(Kate, RoomBinJID). -admin_try_flush_nonexistent_user_bin(Config) -> - %% Check nonexistent domain error - Res = flush_user_bin(<<"user@user.com">>, Config), - ?assertErrMsg(Res, <<"not found">>), - ?assertErrCode(Res, domain_not_found), - %% Check nonexistent user error - User = <<"nonexistent-user@", (domain_helper:domain())/binary>>, - Res2 = flush_user_bin(User, Config), - ?assertErrMsg(Res2, <<"does not exist">>), - ?assertErrCode(Res2, user_does_not_exist). - -admin_try_flush_nonexistent_domain_bin(Config) -> - Res = flush_domain_bin(<<"unknown-domain">>, Config), - ?assertErrMsg(Res, <<"not found">>), - ?assertErrCode(Res, domain_not_found). - admin_try_flush_nonexistent_host_type_bin(Config) -> Res = flush_global_bin(<<"nonexistent host type">>, null, Config), ?assertErrMsg(Res, <<"not found">>), @@ -160,30 +161,31 @@ admin_try_flush_nonexistent_host_type_bin(Config) -> %% Domain admin test cases -domain_admin_try_flush_nonexistent_user_bin(Config) -> - %% Check nonexistent domain error - get_unauthorized(flush_user_bin(<<"user@user.com">>, Config)), - %% Check nonexistent user error - User = <<"nonexistent-user@", (domain_helper:domain())/binary>>, - Res2 = flush_user_bin(User, Config), - ?assertErrMsg(Res2, <<"does not exist">>), - ?assertErrCode(Res2, user_does_not_exist). +domain_admin_flush_user_bin_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_flush_user_bin_no_permission/2). + +domain_admin_flush_user_bin_no_permission(Config, AliceBis) -> + Res = flush_user_bin(AliceBis, Config), + get_unauthorized(Res), + InvalidUser = <<"user@user.com">>, + Res2 = flush_user_bin(InvalidUser, Config), + get_unauthorized(Res2). domain_admin_try_flush_domain_bin_no_permission(Config) -> - get_unauthorized(flush_domain_bin(<<"external-domain">>, Config)). + get_unauthorized(flush_domain_bin(<<"external-domain">>, Config)), + get_unauthorized(flush_domain_bin(domain_helper:secondary_domain(), Config)). domain_admin_flush_global_bin_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}], fun domain_admin_flush_global_bin_no_permission/4). domain_admin_flush_global_bin_no_permission(Config, Alice, AliceBis, Kate) -> + get_unauthorized(flush_global_bin(<<"nonexistent host type">>, null, Config)), SecHostType = domain_helper:host_type(), create_room_and_make_users_leave(Alice, AliceBis, Kate), get_unauthorized(flush_global_bin(SecHostType, null, Config)). -domain_admin_try_flush_nonexistent_host_type_bin(Config) -> - get_unauthorized(flush_global_bin(<<"nonexistent host type">>, null, Config)). - %% User test cases user_flush_own_bin(Config) -> diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 2f842272227..457e378b60d 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -13,6 +13,7 @@ -define(NONEXISTENT_JID, <<"user@user.com">>). -define(DEFAULT_DT, <<"2022-04-17T12:58:30.000000Z">>). +-define(NONEXISTENT_DOMAIN, <<"nonexistent">>). suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -67,9 +68,9 @@ admin_old_users_tests() -> domain_admin_last_tests() -> [admin_set_last, - domain_admin_try_set_nonexistent_user_last, + domain_admin_set_user_last_no_permission, admin_get_last, - domain_admin_get_nonexistent_user_last, + domain_admin_get_user_last_no_permission, admin_try_get_nonexistent_last, admin_count_active_users, domain_admin_try_count_external_domain_active_users]. @@ -258,7 +259,7 @@ admin_remove_old_users_domain_story(Config, Alice, AliceBis, Bob) -> ?assertMatch({ok, _}, check_account(AliceBis)). admin_try_remove_old_users_nonexistent_domain(Config) -> - Res = admin_remove_old_users(<<"nonexistent">>, now_dt_with_offset(0), Config), + Res = admin_remove_old_users(?NONEXISTENT_DOMAIN, now_dt_with_offset(0), Config), ?assertErrMsg(Res, <<"not found">>), ?assertErrCode(Res, domain_not_found). @@ -298,7 +299,7 @@ admin_list_old_users_domain_story(Config, Alice, Bob) -> ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(BobDateTime, second)). admin_try_list_old_users_nonexistent_domain(Config) -> - Res = admin_list_old_users(<<"nonexistent">>, now_dt_with_offset(0), Config), + Res = admin_list_old_users(?NONEXISTENT_DOMAIN, now_dt_with_offset(0), Config), ?assertErrMsg(Res, <<"not found">>), ?assertErrCode(Res, domain_not_found). @@ -335,22 +336,37 @@ admin_logged_user_is_not_old_user_story(Config, _Alice) -> %% Domain admin test cases -domain_admin_try_set_nonexistent_user_last(Config) -> - get_unauthorized(admin_set_last(?NONEXISTENT_JID, <<"status">>, null, Config)). - -domain_admin_get_nonexistent_user_last(Config) -> - get_unauthorized(admin_get_last(?NONEXISTENT_JID, Config)). +domain_admin_set_user_last_no_permission(Config) -> + get_unauthorized(admin_set_last(?NONEXISTENT_JID, <<"status">>, null, Config)), + escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> + BinJID = escalus_client:full_jid(AliceBis), + Resp = admin_set_last(BinJID, <<"status">>, null, Config), + get_unauthorized(Resp) + end). + +domain_admin_get_user_last_no_permission(Config) -> + get_unauthorized(admin_get_last(?NONEXISTENT_JID, Config)), + escalus:fresh_story(Config, [{alice_bis, 1}], fun(AliceBis) -> + BinJID = escalus_client:full_jid(AliceBis), + Resp = admin_get_last(BinJID, Config), + get_unauthorized(Resp) + end). domain_admin_try_count_external_domain_active_users(Config) -> - get_unauthorized(admin_count_active_users(<<"external-domain.com">>, null, Config)). + get_unauthorized(admin_count_active_users(?NONEXISTENT_DOMAIN, null, Config)), + get_unauthorized(admin_count_active_users(domain_helper:secondary_domain(), null, Config)). %% Domain admin old users test cases domain_admin_try_list_old_users_external_domain(Config) -> - get_unauthorized(admin_list_old_users(<<"external">>, now_dt_with_offset(0), Config)). + ExternalDomain = domain_helper:secondary_domain(), + get_unauthorized(admin_list_old_users(?NONEXISTENT_DOMAIN, now_dt_with_offset(0), Config)), + get_unauthorized(admin_list_old_users(ExternalDomain, now_dt_with_offset(0), Config)). domain_admin_try_remove_old_users_external_domain(Config) -> - get_unauthorized(admin_remove_old_users(<<"external">>, now_dt_with_offset(0), Config)). + ExternalDomain = domain_helper:secondary_domain(), + get_unauthorized(admin_remove_old_users(?NONEXISTENT_DOMAIN, now_dt_with_offset(0), Config)), + get_unauthorized(admin_remove_old_users(ExternalDomain, now_dt_with_offset(0), Config)). domain_admin_list_old_users_global(Config) -> jids_with_config(Config, [alice, alice_bis, bob], diff --git a/big_tests/tests/graphql_muc_SUITE.erl b/big_tests/tests/graphql_muc_SUITE.erl index bae74e2e398..a15b97051da 100644 --- a/big_tests/tests/graphql_muc_SUITE.erl +++ b/big_tests/tests/graphql_muc_SUITE.erl @@ -107,52 +107,60 @@ admin_muc_tests() -> domain_admin_muc_tests() -> [admin_create_and_delete_room, admin_try_create_instant_room_with_nonexistent_domain, - domain_admin_try_create_instant_room_with_nonexistent_user, admin_try_delete_nonexistent_room, - domain_admin_try_delete_room_with_nonexistent_domain, + domain_admin_create_and_delete_room_no_permission, admin_list_rooms, + domain_admin_list_rooms_no_permission, admin_list_room_users, - domain_admin_try_list_users_from_nonexistent_room, + domain_admin_list_room_users_no_permission, admin_change_room_config, - domain_admin_try_change_nonexistent_room_config, + domain_admin_change_room_config_no_permission, admin_get_room_config, - domain_admin_try_get_nonexistent_room_config, + domain_admin_get_room_config_no_permission, admin_invite_user, admin_invite_user_with_password, admin_try_invite_user_to_nonexistent_room, + domain_admin_invite_user_no_permission, admin_kick_user, - domain_admin_try_kick_user_from_nonexistent_room, admin_try_kick_user_from_room_without_moderators, + domain_admin_kick_user_no_permission, admin_send_message_to_room, + domain_admin_send_message_to_room_no_permission, admin_send_private_message, + domain_admin_send_private_message_no_permission, admin_get_room_messages, - domain_admin_try_get_nonexistent_room_messages, + domain_admin_get_room_messages_no_permission, admin_set_user_affiliation, - domain_admin_try_set_nonexistent_room_user_affiliation, + domain_admin_set_user_affiliation_no_permission, admin_set_user_role, - domain_admin_try_set_nonexistent_room_user_role, admin_try_set_nonexistent_nick_role, admin_try_set_user_role_in_room_without_moderators, + domain_admin_set_user_role_no_permission, admin_make_user_enter_room, admin_make_user_enter_room_with_password, admin_make_user_enter_room_bare_jid, + domain_admin_make_user_enter_room_no_permission, admin_make_user_exit_room, admin_make_user_exit_room_bare_jid, + domain_admin_make_user_exit_room_no_permission, admin_list_room_affiliations, - domain_admin_try_list_nonexistent_room_affiliations + domain_admin_list_room_affiliations_no_permission ]. init_per_suite(Config) -> HostType = domain_helper:host_type(), + SecondaryHostType = domain_helper:secondary_host_type(), Config2 = escalus:init_per_suite(Config), Config3 = dynamic_modules:save_modules(HostType, Config2), - Config4 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config3), - Config5 = ejabberd_node_utils:init(mim(), Config4), + Config4 = dynamic_modules:save_modules(SecondaryHostType, Config3), + Config5 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config4), + Config6 = ejabberd_node_utils:init(mim(), Config5), dynamic_modules:restart(HostType, mod_disco, config_parser_helper:default_mod_config(mod_disco)), muc_helper:load_muc(), + muc_helper:load_muc(SecondaryHostType), mongoose_helper:ensure_muc_clean(), - Config5. + Config6. end_per_suite(Config) -> escalus_fresh:clean(), @@ -199,6 +207,7 @@ end_per_testcase(TC, Config) -> -define(NONEXISTENT_ROOM, <<"room@room">>). -define(NONEXISTENT_ROOM2, <<"room@", (muc_helper:muc_host())/binary>>). +-define(EXTERNAL_DOMAIN_ROOM, <<"external_room@muc.", (domain_helper:secondary_domain())/binary>>). -define(PASSWORD, <<"pa5sw0rd">>). admin_list_rooms(Config) -> @@ -631,48 +640,145 @@ domain_admin_try_delete_room_with_nonexistent_domain(Config) -> RoomJID = jid:make_bare(<<"unknown">>, <<"unknown">>), get_unauthorized(delete_room(RoomJID, null, Config)). -domain_admin_try_create_instant_room_with_nonexistent_user(Config) -> +domain_admin_create_and_delete_room_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_create_and_delete_room_no_permission_story/2). + +domain_admin_create_and_delete_room_no_permission_story(Config, AliceBis) -> Name = rand_name(), - LocalDomain = domain_helper:domain(), - ExternalDomain = <<"external">>, + ExternalDomain = domain_helper:secondary_domain(), + UnknownDomain = <<"unknown">>, MUCServer = muc_helper:muc_host(), - - LocalJID = <<(rand_name())/binary, "@", LocalDomain/binary>>, - Res1 = create_instant_room(MUCServer, Name, LocalJID, <<"Ali">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res1), <<"not found">>)), + ExternalServer = <<"muc.", ExternalDomain/binary>>, + % Create instant room with a non-existent domain + UnknownJID = <<(rand_name())/binary, "@", UnknownDomain/binary>>, + Res = create_instant_room(MUCServer, Name, UnknownJID, <<"Ali">>, Config), + get_unauthorized(Res), + % Create instant room with an external domain + Res2 = create_instant_room(MUCServer, Name, AliceBis, <<"Ali">>, Config), + get_unauthorized(Res2), + % Delete instant room with a non-existent domain + UnknownRoomJID = jid:make_bare(<<"unknown_room">>, UnknownDomain), + Res3 = delete_room(UnknownRoomJID, null, Config), + get_unauthorized(Res3), + % Delete instant room with an external domain + ExternalRoomJID = jid:make_bare(<<"external_room">>, ExternalServer), + Res4 = delete_room(ExternalRoomJID, null, Config), + get_unauthorized(Res4). + +domain_admin_list_rooms_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_list_rooms_no_permission_story/2). + +domain_admin_list_rooms_no_permission_story(Config, AliceBis) -> + AliceBisJID = jid:from_binary(escalus_client:short_jid(AliceBis)), + AliceBisRoom = rand_name(), + muc_helper:create_instant_room(AliceBisRoom, AliceBisJID, <<"Ali">>, []), + Res = list_rooms(muc_helper:muc_host(), AliceBis, null, null, Config), + get_unauthorized(Res). + +domain_admin_list_room_users_no_permission(Config) -> + get_unauthorized(list_room_users(?NONEXISTENT_ROOM, Config)), + get_unauthorized(list_room_users(?EXTERNAL_DOMAIN_ROOM, Config)). + +domain_admin_change_room_config_no_permission(Config) -> + RoomConfig = #{title => <<"NewTitle">>}, + get_unauthorized(change_room_config(?NONEXISTENT_ROOM, RoomConfig, Config)), + get_unauthorized(change_room_config(?EXTERNAL_DOMAIN_ROOM, RoomConfig, Config)). - ExternalJID = <<(rand_name())/binary, "@", ExternalDomain/binary>>, - Res2 = create_instant_room(MUCServer, Name, ExternalJID, <<"Ali">>, Config), - get_unauthorized(Res2). +domain_admin_get_room_config_no_permission(Config) -> + get_unauthorized(get_room_config(?NONEXISTENT_ROOM, Config)), + get_unauthorized(get_room_config(?EXTERNAL_DOMAIN_ROOM, Config)). -domain_admin_try_list_users_from_nonexistent_room(Config) -> - get_unauthorized(list_room_users(?NONEXISTENT_ROOM, Config)). +domain_admin_invite_user_no_permission(Config) -> + muc_helper:story_with_room(Config, [], [{alice_bis, 1}, {bob, 1}], + fun domain_admin_invite_user_no_permission_story/3). -domain_admin_try_change_nonexistent_room_config(Config) -> - RoomConfig = #{title => <<"NewTitle">>}, - get_unauthorized(change_room_config(?NONEXISTENT_ROOM, RoomConfig, Config)). +domain_admin_invite_user_no_permission_story(Config, Alice, Bob) -> + RoomJIDBin = ?config(room_jid, Config), + RoomJID = jid:from_binary(RoomJIDBin), + Res = invite_user(RoomJID, Alice, Bob, null, Config), + get_unauthorized(Res). -domain_admin_try_get_nonexistent_room_config(Config) -> - get_unauthorized(get_room_config(?NONEXISTENT_ROOM, Config)). +domain_admin_kick_user_no_permission(Config) -> + get_unauthorized(kick_user(?NONEXISTENT_ROOM, <<"ali">>, null, Config)), + get_unauthorized(kick_user(?EXTERNAL_DOMAIN_ROOM, <<"ali">>, null, Config)). -domain_admin_try_kick_user_from_nonexistent_room(Config) -> - get_unauthorized(kick_user(?NONEXISTENT_ROOM, <<"ali">>, null, Config)). +domain_admin_send_message_to_room_no_permission(Config) -> + muc_helper:story_with_room(Config, [], [{alice_bis, 1}], + fun domain_admin_send_message_to_room_no_permission_story/2). -domain_admin_try_get_nonexistent_room_messages(Config) -> - get_unauthorized(get_room_messages(?NONEXISTENT_ROOM, null, null, Config)). +domain_admin_send_message_to_room_no_permission_story(Config, AliceBis) -> + RoomJID = jid:from_binary(?config(room_jid, Config)), + Message = <<"Hello All!">>, + AliceNick = <<"Bobek">>, + enter_room(RoomJID, AliceBis, AliceNick), + escalus:wait_for_stanza(AliceBis), + % Send message + Res = send_message_to_room(RoomJID, AliceBis, Message, Config), + get_unauthorized(Res). -domain_admin_try_set_nonexistent_room_user_affiliation(Config) -> +domain_admin_send_private_message_no_permission(Config) -> + muc_helper:story_with_room(Config, [], [{alice_bis, 1}, {bob, 1}], + fun domain_admin_send_private_message_no_permission_story/3). + +domain_admin_send_private_message_no_permission_story(Config, AliceBis, Bob) -> + RoomJID = jid:from_binary(?config(room_jid, Config)), + Message = <<"Hello Bob!">>, + BobNick = <<"Bobek">>, + AliceBisNick = <<"Ali">>, + enter_room(RoomJID, AliceBis, AliceBisNick), + enter_room(RoomJID, Bob, BobNick), + escalus:wait_for_stanzas(Bob, 2), + % Send message + Res = send_private_message(RoomJID, AliceBis, BobNick, Message, Config), + get_unauthorized(Res). + +domain_admin_get_room_messages_no_permission(Config) -> + get_unauthorized(get_room_messages(?NONEXISTENT_ROOM, null, null, Config)), + get_unauthorized(get_room_messages(?EXTERNAL_DOMAIN_ROOM, null, null, Config)). + +domain_admin_set_user_affiliation_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], - fun domain_admin_try_set_nonexistent_room_user_affiliation/2). + fun domain_admin_set_user_affiliation_no_permission_story/2). + +domain_admin_set_user_affiliation_no_permission_story(Config, Alice) -> + get_unauthorized(set_user_affiliation(?NONEXISTENT_ROOM, Alice, admin, Config)), + get_unauthorized(set_user_affiliation(?EXTERNAL_DOMAIN_ROOM, Alice, admin, Config)). + +domain_admin_set_user_role_no_permission(Config) -> + get_unauthorized(set_user_role(?NONEXISTENT_ROOM, <<"Alice">>, moderator, Config)), + get_unauthorized(set_user_role(?EXTERNAL_DOMAIN_ROOM, <<"Alice">>, moderator, Config)). + +domain_admin_list_room_affiliations_no_permission(Config) -> + get_unauthorized(list_room_affiliations(?NONEXISTENT_ROOM, null, Config)), + get_unauthorized(list_room_affiliations(?EXTERNAL_DOMAIN_ROOM, null, Config)). -domain_admin_try_set_nonexistent_room_user_affiliation(Config, Alice) -> - get_unauthorized(set_user_affiliation(?NONEXISTENT_ROOM, Alice, admin, Config)). +domain_admin_make_user_enter_room_no_permission(Config) -> + muc_helper:story_with_room(Config, [], [{alice_bis, 1}], + fun domain_admin_make_user_enter_room_no_permission_story/2). -domain_admin_try_set_nonexistent_room_user_role(Config) -> - get_unauthorized(set_user_role(?NONEXISTENT_ROOM, <<"Alice">>, moderator, Config)). +domain_admin_make_user_enter_room_no_permission_story(Config, AliceBis) -> + RoomJID = jid:from_binary(?config(room_jid, Config)), + Nick = <<"ali">>, + % Enter room without password + Res = enter_room(RoomJID, AliceBis, Nick, null, Config), + get_unauthorized(Res), + % Enter room with password + Res2 = enter_room(RoomJID, AliceBis, Nick, ?PASSWORD, Config), + get_unauthorized(Res2). + +domain_admin_make_user_exit_room_no_permission(Config) -> + muc_helper:story_with_room(Config, [{persistent, true}], [{alice_bis, 1}], + fun domain_admin_make_user_exit_room_no_permission_story/2). -domain_admin_try_list_nonexistent_room_affiliations(Config) -> - get_unauthorized(list_room_affiliations(?NONEXISTENT_ROOM, null, Config)). +domain_admin_make_user_exit_room_no_permission_story(Config, AliceBis) -> + RoomJID = jid:from_binary(?config(room_jid, Config)), + Nick = <<"ali">>, + enter_room(RoomJID, AliceBis, Nick), + ?assertMatch([_], get_room_users(RoomJID)), + Res = exit_room(RoomJID, AliceBis, Nick, Config), + get_unauthorized(Res). %% User test cases diff --git a/big_tests/tests/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl index d6d8bd562c2..6d50167f6b9 100644 --- a/big_tests/tests/graphql_muc_light_SUITE.erl +++ b/big_tests/tests/graphql_muc_light_SUITE.erl @@ -73,41 +73,63 @@ admin_muc_light_tests() -> admin_change_room_config, admin_change_room_config_with_custom_fields, admin_change_room_config_errors, + admin_change_room_config_non_existent_domain, admin_invite_user, admin_invite_user_errors, admin_delete_room, + admin_delete_room_non_existent_domain, admin_kick_user, admin_send_message_to_room, admin_send_message_to_room_errors, admin_get_room_messages, + admin_get_room_messages_non_existent_domain, admin_list_user_rooms, + admin_list_user_rooms_non_existent_domain, admin_list_room_users, + admin_list_room_users_non_existent_domain, admin_get_room_config, - admin_blocking_list + admin_get_room_config_non_existent_domain, + admin_blocking_list, + admin_blocking_list_errors ]. domain_admin_muc_light_tests() -> [admin_create_room, - domain_admin_create_identified_room, + admin_create_room_with_custom_fields, + domain_admin_create_room_no_permission, + admin_create_identified_room, + domain_admin_create_identified_room_no_permission, admin_change_room_config, - domain_admin_change_room_config_errors, + admin_change_room_config_with_custom_fields, + admin_change_room_config_errors, + domain_admin_change_room_config_no_permission, admin_invite_user, admin_invite_user_errors, - domain_admin_delete_room, + domain_admin_invite_user_no_permission, + admin_delete_room, + domain_admin_delete_room_no_permission, admin_kick_user, + domain_admin_kick_user_no_permission, admin_send_message_to_room, admin_send_message_to_room_errors, - domain_admin_get_room_messages, - domain_admin_list_user_rooms, - domain_admin_list_room_users, - domain_admin_get_room_config, - domain_admin_blocking_list + domain_admin_send_message_to_room_no_permission, + admin_get_room_messages, + domain_admin_get_room_messages_no_permission, + admin_list_user_rooms, + domain_admin_list_user_rooms_no_permission, + admin_list_room_users, + domain_admin_list_room_users_no_permission, + admin_get_room_config, + domain_admin_get_room_config_no_permission, + admin_blocking_list, + domain_admin_blocking_list_no_permission ]. init_per_suite(Config) -> Config1 = init_modules(Config), Config2 = ejabberd_node_utils:init(mim(), Config1), - [{muc_light_host, muc_light_helper:muc_host()} + [{muc_light_host, muc_light_helper:muc_host()}, + {secondary_muc_light_host, <<"muclight.", (domain_helper:secondary_domain())/binary>>} | escalus:init_per_suite(Config2)]. end_per_suite(Config) -> @@ -117,10 +139,13 @@ end_per_suite(Config) -> init_modules(Config) -> HostType = domain_helper:host_type(), + SecondaryHostType = domain_helper:secondary_host_type(), Config1 = dynamic_modules:save_modules(HostType, Config), - Config2 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config1), + Config2 = dynamic_modules:save_modules(SecondaryHostType, Config1), + Config3 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config2), dynamic_modules:ensure_modules(HostType, required_modules(suite)), - Config2. + dynamic_modules:ensure_modules(SecondaryHostType, required_modules(suite)), + Config3. required_modules(_) -> MucLightOpts = mod_config(mod_muc_light, #{rooms_in_rosters => true, @@ -561,211 +586,216 @@ user_blocking_list_story(Config, Alice, Bob) -> %% Domain admin test cases -domain_admin_create_identified_room(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], - fun domain_admin_create_identified_room_story/2). +domain_admin_create_room_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_create_room_no_permission_story/2). -domain_admin_create_identified_room_story(Config, Alice) -> - AliceBin = escalus_client:short_jid(Alice), +domain_admin_create_room_no_permission_story(Config, AliceBis) -> + AliceBisBin = escalus_client:short_jid(AliceBis), + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), MucServer = ?config(muc_light_host, Config), - Name = <<"domain admin room">>, + Name = <<"first room">>, Subject = <<"testing">>, - Id = <<"domain_admin_room_", (atom_to_binary(?config(protocol, Config)))/binary>>, - Res = create_room(MucServer, Name, AliceBin, Subject, Id, Config), - #{<<"jid">> := JID, <<"name">> := Name, <<"subject">> := Subject} = - get_ok_value(?CREATE_ROOM_PATH, Res), - ?assertMatch(#jid{luser = Id, lserver = MucServer}, jid:from_binary(JID)), - % Create a room with an existing ID - Res2 = create_room(MucServer, <<"snd room">>, AliceBin, Subject, Id, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"already exists">>)), % Try with a non-existent domain - Res3 = create_room(?UNKNOWN_DOMAIN, <<"name">>, AliceBin, Subject, Id, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)), - % Try with an empty string passed as ID - Res4 = create_room(MucServer, <<"name">>, AliceBin, Subject, <<>>, Config), - ?assertNotEqual(nomatch, binary:match(get_coercion_err_msg(Res4), <<"Given string is empty">>)). + Res = create_room(MucServer, Name, InvalidUser, Subject, null, Config), + get_unauthorized(Res), + % Try with an external domain + Res2 = create_room(MucServer, Name, AliceBisBin, Subject, null, Config), + get_unauthorized(Res2). + +domain_admin_create_identified_room_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_create_identified_room_no_permission_story/2). + +domain_admin_create_identified_room_no_permission_story(Config, AliceBis) -> + AliceBisBin = escalus_client:short_jid(AliceBis), + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), + MucServer = ?config(muc_light_host, Config), + Name = <<"first room">>, + Subject = <<"testing">>, + SchemaEndpoint = atom_to_binary(?config(schema_endpoint, Config)), + Protocol = atom_to_binary(?config(protocol, Config)), + Id = <<"my_room_", SchemaEndpoint/binary, "_", Protocol/binary>>, + % Try with a non-existent domain + Res = create_room(MucServer, Name, InvalidUser, Subject, Id, Config), + get_unauthorized(Res), + % Try with an external domain + Res2 = create_room(MucServer, Name, AliceBisBin, Subject, Id, Config), + get_unauthorized(Res2). -domain_admin_change_room_config_errors(Config) -> +domain_admin_change_room_config_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], - fun domain_admin_change_room_config_errors_story/3). + fun domain_admin_change_room_config_no_permission_story/3). -domain_admin_change_room_config_errors_story(Config, Alice, Bob) -> +domain_admin_change_room_config_no_permission_story(Config, Alice, Bob) -> AliceBin = escalus_client:short_jid(Alice), BobBin = escalus_client:short_jid(Bob), - MUCServer = ?config(muc_light_host, Config), + MUCServer = ?config(secondary_muc_light_host, Config), RoomName = <<"first room">>, {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), {ok, _} = invite_user(RoomJID, AliceBin, BobBin), - % Try to change the config with a non-existent domain - Res = change_room_configuration( - make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), + RoomJIDBin = jid:to_binary(RoomJID), + Res = change_room_configuration(RoomJIDBin, AliceBin, RoomName, <<"subject">>, Config), get_unauthorized(Res), - % Try to change the config of the non-existent room + % Try to change the config with an external domain Res2 = change_room_configuration( - make_bare_jid(<<"unknown">>, MUCServer), AliceBin, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), - % Try to change the config by the non-existent user - Res3 = change_room_configuration( - jid:to_binary(RoomJID), <<"wrong-user@wrong-domain">>, RoomName, <<"subject2">>, - Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not occupy this room">>)), - % Try to change a config by the user without permission - Res4 = change_room_configuration( - jid:to_binary(RoomJID), BobBin, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res4), - <<"does not have permission to change">>)). + make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), + get_unauthorized(Res2). -domain_admin_delete_room(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_delete_room_story/2). - -domain_admin_delete_room_story(Config, Alice) -> - AliceBin = escalus_client:short_jid(Alice), +domain_admin_invite_user_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_invite_user_no_permission_story/3). + +domain_admin_invite_user_no_permission_story(Config, AliceBis, Bob) -> + AliceBisBin = escalus_client:short_jid(AliceBis), + BobBin = escalus_client:short_jid(Bob), MUCServer = ?config(muc_light_host, Config), Name = <<"first room">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, Name, <<"subject2">>, AliceBisBin), + Res = invite_user(jid:to_binary(RoomJID), AliceBisBin, BobBin, Config), + get_unauthorized(Res). + +domain_admin_delete_room_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_delete_room_no_permission_story/2). + +domain_admin_delete_room_no_permission_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + RoomName = <<"first room">>, + MUCServer = ?config(secondary_muc_light_host, Config), {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = - create_room(MUCServer, Name, <<"subject">>, AliceBin), - Res = delete_room(jid:to_binary(RoomJID), Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(?DELETE_ROOM_PATH, Res), - <<"successfully">>)), - ?assertEqual({error, not_exists}, get_room_info(jid:from_binary(RoomJID))), + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + RoomJIDBin = jid:to_binary(RoomJID), + Res = delete_room(RoomJIDBin, Config), + get_unauthorized(Res), % Try with a non-existent domain Res2 = delete_room(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), - get_unauthorized(Res2), - % Try with a non-existent room - Res3 = delete_room(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"Cannot remove">>)). + get_unauthorized(Res2). -domain_admin_get_room_messages(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], - fun domain_admin_get_room_messages_story/2). +domain_admin_kick_user_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_kick_user_no_permission_story/3). -domain_admin_get_room_messages_story(Config, Alice) -> +domain_admin_kick_user_no_permission_story(Config, Alice, Bob) -> AliceBin = escalus_client:short_jid(Alice), - %Domain = escalus_client:server(Alice), - MUCServer = ?config(muc_light_host, Config), + BobBin = escalus_client:short_jid(Bob), RoomName = <<"first room">>, - RoomName2 = <<"second room">>, + MUCServer = ?config(secondary_muc_light_host, Config), {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), - {ok, _} = create_room(MUCServer, RoomName2, <<"subject">>, AliceBin), - Message = <<"Hello friends">>, - send_message_to_room(RoomJID, jid:from_binary(AliceBin), Message), - mam_helper:maybe_wait_for_archive(Config), - % Get messages so far - Limit = 40, - Res = get_room_messages(jid:to_binary(RoomJID), Limit, null, Config), - #{<<"stanzas">> := [#{<<"stanza">> := StanzaXML}], <<"limit">> := Limit} = - get_ok_value(?GET_MESSAGES_PATH, Res), - ?assertMatch({ok, #xmlel{name = <<"message">>}}, exml:parse(StanzaXML)), - % Get messages before the given date and time - Before = <<"2022-02-17T04:54:13+00:00">>, - Res2 = get_room_messages(jid:to_binary(RoomJID), null, Before, Config), - ?assertMatch(#{<<"stanzas">> := [], <<"limit">> := 50}, get_ok_value(?GET_MESSAGES_PATH, Res2)), - % Try to pass too big page size value - Res3 = get_room_messages(jid:to_binary(RoomJID), 51, Before, Config), - ?assertMatch(#{<<"limit">> := 50},get_ok_value(?GET_MESSAGES_PATH, Res3)), + {ok, _} = invite_user(RoomJID, AliceBin, BobBin), + RoomJIDBin = jid:to_binary(RoomJID), + ?assertEqual(2, length(get_room_aff(RoomJID))), + Res = kick_user(RoomJIDBin, BobBin, Config), + get_unauthorized(Res), % Try with a non-existent domain - Res4 = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), - get_unauthorized(Res4). + Res2 = kick_user(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), BobBin, Config), + get_unauthorized(Res2). -domain_admin_list_user_rooms(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_list_user_rooms_story/2). +domain_admin_send_message_to_room_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_send_message_to_room_no_permission_story/3). -domain_admin_list_user_rooms_story(Config, Alice) -> - AliceBin = escalus_client:short_jid(Alice), - Domain = escalus_client:server(Alice), +domain_admin_send_message_to_room_no_permission_story(Config, AliceBis, Bob) -> + AliceBisBin = escalus_client:short_jid(AliceBis), + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), + BobBin = escalus_client:short_jid(Bob), MUCServer = ?config(muc_light_host, Config), RoomName = <<"first room">>, - RoomName2 = <<"second room">>, - {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), - {ok, #{jid := RoomJID2}} = create_room(MUCServer, RoomName2, <<"subject">>, AliceBin), - Res = list_user_rooms(AliceBin, Config), - ?assertEqual(lists:sort([jid:to_binary(RoomJID), jid:to_binary(RoomJID2)]), - lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res))), - % Try with a non-existent user - Res2 = list_user_rooms(<<"not-exist@", Domain/binary>>, Config), - ?assertEqual([], lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res2))), + MsgBody = <<"Hello there!">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBisBin), + {ok, _} = invite_user(RoomJID, AliceBisBin, BobBin), % Try with a non-existent domain - Res3 = list_user_rooms(<<"not-exist@not-exist">>, Config), - get_unauthorized(Res3). + Res = send_message_to_room(jid:to_binary(RoomJID), InvalidUser, MsgBody, Config), + get_unauthorized(Res), + % Try with an external domain room + Res2 = send_message_to_room(jid:to_binary(RoomJID), AliceBisBin, MsgBody, Config), + get_unauthorized(Res2). -domain_admin_list_room_users(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_list_room_users_story/2). +domain_admin_get_room_messages_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_get_room_messages_no_permission_story/2). -domain_admin_list_room_users_story(Config, Alice) -> +domain_admin_get_room_messages_no_permission_story(Config, Alice) -> AliceBin = escalus_client:short_jid(Alice), - AliceLower = escalus_utils:jid_to_lower(AliceBin), - MUCServer = ?config(muc_light_host, Config), + MUCServer = ?config(secondary_muc_light_host, Config), RoomName = <<"first room">>, - {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), - Res = list_room_users(jid:to_binary(RoomJID), Config), - ?assertEqual([#{<<"jid">> => AliceLower, <<"affiliation">> => <<"OWNER">>}], - get_ok_value(?LIST_ROOM_USERS_PATH, Res)), + Limit = 40, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + RoomJIDBin = jid:to_binary(RoomJID), + Res = get_room_messages(RoomJIDBin, Limit, null, Config), + get_unauthorized(Res), % Try with a non-existent domain - Res2 = list_room_users(make_bare_jid(RoomJID#jid.luser, ?UNKNOWN_DOMAIN), Config), - get_unauthorized(Res2), - % Try with a non-existent room - Res3 = list_room_users(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + Res2 = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), + get_unauthorized(Res2). -domain_admin_get_room_config(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}], fun domain_admin_get_room_config_story/2). +domain_admin_list_user_rooms_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_list_user_rooms_no_permission_story/2). -domain_admin_get_room_config_story(Config, Alice) -> +domain_admin_list_user_rooms_no_permission_story(Config, AliceBis) -> + AliceBisBin = escalus_client:short_jid(AliceBis), + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), + % Try with a non-existent domain + Res = list_user_rooms(InvalidUser, Config), + get_unauthorized(Res), + % Try with an external domain + Res2 = list_user_rooms(AliceBisBin, Config), + get_unauthorized(Res2). + +domain_admin_list_room_users_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_list_room_users_story_no_permission/2). + +domain_admin_list_room_users_story_no_permission(Config, Alice) -> AliceBin = escalus_client:short_jid(Alice), - AliceLower = escalus_utils:jid_to_lower(AliceBin), - MUCServer = ?config(muc_light_host, Config), + MUCServer = ?config(secondary_muc_light_host, Config), RoomName = <<"first room">>, - RoomSubject = <<"Room about nothing">>, {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = - create_room(MUCServer, RoomName, RoomSubject, AliceBin), + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), RoomJIDBin = jid:to_binary(RoomJID), - Res = get_room_config(jid:to_binary(RoomJID), Config), - ?assertEqual(#{<<"jid">> => RoomJIDBin, <<"subject">> => RoomSubject, <<"name">> => RoomName, - <<"options">> => [#{<<"key">> => <<"background">>, <<"value">> => <<>>}, - #{<<"key">> => <<"music">>, <<"value">> => <<>>}, - #{<<"key">> => <<"roomname">>, <<"value">> => RoomName}, - #{<<"key">> => <<"subject">>, <<"value">> => RoomSubject}], - <<"participants">> => [#{<<"jid">> => AliceLower, - <<"affiliation">> => <<"OWNER">>}]}, - get_ok_value([data, muc_light, getRoomConfig], Res)), + Res = list_room_users(RoomJIDBin, Config), + get_unauthorized(Res), + % Try with an external domain + Res2 = list_room_users(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), + get_unauthorized(Res2). + +domain_admin_get_room_config_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun domain_admin_get_room_config_no_permission_story/2). + +domain_admin_get_room_config_no_permission_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(secondary_muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + RoomJIDBin = jid:to_binary(RoomJID), + Res = get_room_config(RoomJIDBin, Config), + get_unauthorized(Res), % Try with a non-existent domain Res2 = get_room_config(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), - get_unauthorized(Res2), - % Try with a non-existent room - Res3 = get_room_config(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + get_unauthorized(Res2). -domain_admin_blocking_list(Config) -> - escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], - fun domain_admin_blocking_list_story/3). +domain_admin_blocking_list_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_blocking_list_no_permission_story/2). -domain_admin_blocking_list_story(Config, Alice, Bob) -> - AliceBin = escalus_client:full_jid(Alice), - BobBin = escalus_client:full_jid(Bob), - BobShortBin = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), - Res = get_user_blocking(AliceBin, Config), - ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res)), - Res2 = set_blocking(AliceBin, [{<<"USER">>, <<"DENY">>, BobBin}], Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(?SET_BLOCKING_LIST_PATH, Res2), - <<"successfully">>)), - Res3 = get_user_blocking(AliceBin, Config), - ?assertEqual([#{<<"entityType">> => <<"USER">>, - <<"action">> => <<"DENY">>, - <<"entity">> => BobShortBin}], - get_ok_value(?GET_BLOCKING_LIST_PATH, Res3)), - Res4 = set_blocking(AliceBin, [{<<"USER">>, <<"ALLOW">>, BobBin}], Config), - ?assertNotEqual(nomatch, binary:match(get_ok_value(?SET_BLOCKING_LIST_PATH, Res4), - <<"successfully">>)), - Res5 = get_user_blocking(AliceBin, Config), - ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)), - % Check whether errors are handled correctly +domain_admin_blocking_list_no_permission_story(Config, AliceBis) -> + AliceBisBin = escalus_client:full_jid(AliceBis), InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), - Res6 = get_user_blocking(InvalidUser, Config), - get_unauthorized(Res6), - Res7 = set_blocking(InvalidUser, [], Config), - get_unauthorized(Res7). + % Try with a non-existent user + Res = get_user_blocking(InvalidUser, Config), + get_unauthorized(Res), + Res2 = set_blocking(InvalidUser, [], Config), + get_unauthorized(Res2), + % Try with an external domain user + Res3 = get_user_blocking(AliceBisBin, Config), + get_unauthorized(Res3), + Res4 = set_blocking(AliceBisBin, [], Config), + get_unauthorized(Res4). %% Admin test cases @@ -790,13 +820,14 @@ admin_blocking_list_story(Config, Alice, Bob) -> ?assertNotEqual(nomatch, binary:match(get_ok_value(?SET_BLOCKING_LIST_PATH, Res4), <<"successfully">>)), Res5 = get_user_blocking(AliceBin, Config), - ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)), - % Check whether errors are handled correctly + ?assertMatch([], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)). + +admin_blocking_list_errors(Config) -> InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), - Res6 = get_user_blocking(InvalidUser, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res6), <<"not found">>)), - Res7 = set_blocking(InvalidUser, [], Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res7), <<"not found">>)). + Res = get_user_blocking(InvalidUser, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)), + Res2 = set_blocking(InvalidUser, [], Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)). admin_create_room(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_create_room_story/2). @@ -843,7 +874,9 @@ admin_create_identified_room_story(Config, Alice) -> MucServer = ?config(muc_light_host, Config), Name = <<"first room">>, Subject = <<"testing">>, - Id = <<"my_room_", (atom_to_binary(?config(protocol, Config)))/binary>>, + SchemaEndpoint = atom_to_binary(?config(schema_endpoint, Config)), + Protocol = atom_to_binary(?config(protocol, Config)), + Id = <<"my_room_", SchemaEndpoint/binary, "_", Protocol/binary>>, Res = create_room(MucServer, Name, AliceBin, Subject, Id, Config), #{<<"jid">> := JID, <<"name">> := Name, <<"subject">> := Subject} = get_ok_value(?CREATE_ROOM_PATH, Res), @@ -912,25 +945,37 @@ admin_change_room_config_errors_story(Config, Alice, Bob) -> {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), {ok, _} = invite_user(RoomJID, AliceBin, BobBin), - % Try to change the config with a non-existent domain - Res = change_room_configuration( - make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)), % Try to change the config of the non-existent room - Res2 = change_room_configuration( + Res = change_room_configuration( make_bare_jid(<<"unknown">>, MUCServer), AliceBin, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)), % Try to change the config by the non-existent user - Res3 = change_room_configuration( + Res2 = change_room_configuration( jid:to_binary(RoomJID), <<"wrong-user@wrong-domain">>, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not occupy this room">>)), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not occupy this room">>)), % Try to change a config by the user without permission - Res4 = change_room_configuration( + Res3 = change_room_configuration( jid:to_binary(RoomJID), BobBin, RoomName, <<"subject2">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res4), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"does not have permission to change">>)). +admin_change_room_config_non_existent_domain(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_change_room_config_non_existent_domain_story/3). + +admin_change_room_config_non_existent_domain_story(Config, Alice, Bob) -> + AliceBin = escalus_client:short_jid(Alice), + BobBin = escalus_client:short_jid(Bob), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + {ok, _} = invite_user(RoomJID, AliceBin, BobBin), + Res = change_room_configuration( + make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). + admin_invite_user(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_invite_user_story/3). @@ -984,12 +1029,22 @@ admin_delete_room_story(Config, Alice) -> ?assertNotEqual(nomatch, binary:match(get_ok_value(?DELETE_ROOM_PATH, Res), <<"successfully">>)), ?assertEqual({error, not_exists}, get_room_info(jid:from_binary(RoomJID))), - % Try with a non-existent domain - Res2 = delete_room(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), % Try with a non-existent room - Res3 = delete_room(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"Cannot remove">>)). + Res2 = delete_room(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"Cannot remove">>)). + +admin_delete_room_non_existent_domain(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_delete_room_non_existent_domain_story/2). + +admin_delete_room_non_existent_domain_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(muc_light_host, Config), + Name = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID}}} = + create_room(MUCServer, Name, <<"subject">>, AliceBin), + Res = delete_room(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). admin_kick_user(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_kick_user_story/3). @@ -1074,10 +1129,21 @@ admin_get_room_messages_story(Config, Alice) -> ?assertMatch(#{<<"stanzas">> := [], <<"limit">> := 50}, get_ok_value(?GET_MESSAGES_PATH, Res2)), % Try to pass too big page size value Res3 = get_room_messages(jid:to_binary(RoomJID), 51, Before, Config), - ?assertMatch(#{<<"limit">> := 50},get_ok_value(?GET_MESSAGES_PATH, Res3)), - % Try with a non-existent domain - Res4 = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res4), <<"not found">>)). + ?assertMatch(#{<<"limit">> := 50},get_ok_value(?GET_MESSAGES_PATH, Res3)). + +admin_get_room_messages_non_existent_domain(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_room_messages_non_existent_domain_story/2). + +admin_get_room_messages_non_existent_domain_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID}}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + Limit = 40, + Res = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). admin_list_user_rooms(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_list_user_rooms_story/2). @@ -1095,10 +1161,11 @@ admin_list_user_rooms_story(Config, Alice) -> lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res))), % Try with a non-existent user Res2 = list_user_rooms(<<"not-exist@", Domain/binary>>, Config), - ?assertEqual([], lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res2))), - % Try with a non-existent domain - Res3 = list_user_rooms(<<"not-exist@not-exist">>, Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + ?assertEqual([], lists:sort(get_ok_value(?LIST_USER_ROOMS_PATH, Res2))). + +admin_list_user_rooms_non_existent_domain(Config) -> + Res = list_user_rooms(<<"not-exist@not-exist">>, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). admin_list_room_users(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_list_room_users_story/2). @@ -1112,12 +1179,21 @@ admin_list_room_users_story(Config, Alice) -> Res = list_room_users(jid:to_binary(RoomJID), Config), ?assertEqual([#{<<"jid">> => AliceLower, <<"affiliation">> => <<"OWNER">>}], get_ok_value(?LIST_ROOM_USERS_PATH, Res)), - % Try with a non-existent domain - Res2 = list_room_users(make_bare_jid(RoomJID#jid.luser, ?UNKNOWN_DOMAIN), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), % Try with a non-existent room - Res3 = list_room_users(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + Res2 = list_room_users(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)). + +admin_list_room_users_non_existent_domain(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_list_room_users_non_existent_domain_story/2). + +admin_list_room_users_non_existent_domain_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + Res = list_room_users(make_bare_jid(RoomJID#jid.luser, ?UNKNOWN_DOMAIN), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). admin_get_room_config(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_get_room_config_story/2). @@ -1131,7 +1207,7 @@ admin_get_room_config_story(Config, Alice) -> {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = create_room(MUCServer, RoomName, RoomSubject, AliceBin), RoomJIDBin = jid:to_binary(RoomJID), - Res = get_room_config(jid:to_binary(RoomJID), Config), + Res = get_room_config(RoomJIDBin, Config), ?assertEqual(#{<<"jid">> => RoomJIDBin, <<"subject">> => RoomSubject, <<"name">> => RoomName, <<"options">> => [#{<<"key">> => <<"background">>, <<"value">> => <<>>}, #{<<"key">> => <<"music">>, <<"value">> => <<>>}, @@ -1140,12 +1216,22 @@ admin_get_room_config_story(Config, Alice) -> <<"participants">> => [#{<<"jid">> => AliceLower, <<"affiliation">> => <<"OWNER">>}]}, get_ok_value([data, muc_light, getRoomConfig], Res)), - % Try with a non-existent domain - Res2 = get_room_config(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)), % Try with a non-existent room - Res3 = get_room_config(make_bare_jid(?UNKNOWN, MUCServer), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"not found">>)). + Res2 = get_room_config(make_bare_jid(?UNKNOWN, MUCServer), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"not found">>)). + +admin_get_room_config_non_existent_domain(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_room_config_non_existent_domain_story/2). + +admin_get_room_config_non_existent_domain_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(muc_light_host, Config), + RoomName = <<"first room">>, + {ok, #{jid := #jid{luser = RoomID}}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + Res = get_room_config(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). %% Helpers diff --git a/big_tests/tests/graphql_offline_SUITE.erl b/big_tests/tests/graphql_offline_SUITE.erl index ee8cc2e8395..f012dc96e07 100644 --- a/big_tests/tests/graphql_offline_SUITE.erl +++ b/big_tests/tests/graphql_offline_SUITE.erl @@ -55,8 +55,8 @@ domain_admin_offline_tests() -> admin_delete_old_messages_test, admin_delete_expired_messages2_test, admin_delete_old_messages2_test, - domain_admin_delete_expired_messages_no_domain_test, - domain_admin_delete_old_messages_no_domain_test]. + domain_admin_delete_expired_messages_no_permission_test, + domain_admin_delete_old_messages_no_permission_test]. domain_admin_offline_not_configured_tests() -> [admin_delete_expired_messages_offline_not_configured_test, @@ -84,13 +84,8 @@ init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin, Config) -> graphql_helper:init_domain_admin_handler(Config); -init_per_group(admin_offline, Config) -> - HostType = host_type(), - Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), - ModConfig = create_config(Backend), - dynamic_modules:ensure_modules(HostType, ModConfig), - [{backend, Backend} | escalus:init_per_suite(Config)]; -init_per_group(domain_admin_offline, Config) -> +init_per_group(GroupName, Config) when GroupName =:= admin_offline; + GroupName =:= domain_admin_offline -> HostType = host_type(), Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), ModConfig = create_config(Backend), @@ -170,11 +165,13 @@ admin_delete_old_messages_offline_not_configured_test(Config) -> %% Domain admin test cases -domain_admin_delete_expired_messages_no_domain_test(Config) -> - get_unauthorized(delete_expired_messages(<<"AAAA">>, Config)). +domain_admin_delete_expired_messages_no_permission_test(Config) -> + get_unauthorized(delete_expired_messages(<<"AAAA">>, Config)), + get_unauthorized(delete_expired_messages(domain_helper:secondary_domain(), Config)). -domain_admin_delete_old_messages_no_domain_test(Config) -> - get_unauthorized(delete_old_messages(<<"AAAA">>, 2, Config)). +domain_admin_delete_old_messages_no_permission_test(Config) -> + get_unauthorized(delete_old_messages(<<"AAAA">>, 2, Config)), + get_unauthorized(delete_old_messages(domain_helper:secondary_domain(), 2, Config)). %% Commands diff --git a/big_tests/tests/graphql_private_SUITE.erl b/big_tests/tests/graphql_private_SUITE.erl index 8aeb2a4552c..42169f2e8a4 100644 --- a/big_tests/tests/graphql_private_SUITE.erl +++ b/big_tests/tests/graphql_private_SUITE.erl @@ -32,8 +32,8 @@ user_private_tests() -> domain_admin_private_tests() -> [admin_set_private, admin_get_private, - domain_admin_no_user_error_set, - domain_admin_no_user_error_get]. + domain_admin_user_set_private_no_permission, + domain_admin_user_get_private_no_permission]. admin_private_tests() -> [admin_set_private, @@ -157,14 +157,26 @@ private_input() -> %% Domain admin tests -domain_admin_no_user_error_set(Config) -> +domain_admin_user_set_private_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_user_set_private_no_permission/2). +domain_admin_user_set_private_no_permission(Config, AliceBis) -> ElemStr = exml:to_binary(private_input()), - Result = admin_set_private(<<"AAAAA">>, ElemStr, Config), - get_unauthorized(Result). - -domain_admin_no_user_error_get(Config) -> - Result = admin_get_private(<<"AAAAA">>, <<"my_element">>, <<"alice:private:ns">>, Config), - get_unauthorized(Result). + Result = admin_set_private(user_to_bin(AliceBis), ElemStr, Config), + get_unauthorized(Result), + Result2 = admin_set_private(<<"AAAAA">>, ElemStr, Config), + get_unauthorized(Result2). + +domain_admin_user_get_private_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_user_get_private_no_permission/2). + +domain_admin_user_get_private_no_permission(Config, AliceBis) -> + AliceBisBin = user_to_bin(AliceBis), + Result = admin_get_private(AliceBisBin, <<"my_element">>, <<"alice:private:ns">>, Config), + get_unauthorized(Result), + Result2 = admin_get_private(<<"AAAAA">>, <<"my_element">>, <<"alice:private:ns">>, Config), + get_unauthorized(Result2). %% Commands diff --git a/big_tests/tests/graphql_session_SUITE.erl b/big_tests/tests/graphql_session_SUITE.erl index 28f03efc05b..f7ff33905e7 100644 --- a/big_tests/tests/graphql_session_SUITE.erl +++ b/big_tests/tests/graphql_session_SUITE.erl @@ -45,14 +45,19 @@ domain_admin_session_tests() -> [domain_admin_list_sessions, domain_admin_count_sessions, admin_list_user_sessions, + domain_admin_list_user_sessions_no_permission, admin_count_user_resources, + domain_admin_count_user_resources_no_permission, admin_get_user_resource, + domain_admin_get_user_resource_no_permission, domain_admin_list_users_with_status, domain_admin_count_users_with_status, admin_kick_session, + domain_admin_kick_user_no_permission, admin_set_presence, admin_set_presence_away, - admin_set_presence_unavailable]. + admin_set_presence_unavailable, + domain_admin_set_presence_no_permission]. init_per_suite(Config) -> Config1 = ejabberd_node_utils:init(mim(), Config), @@ -163,6 +168,56 @@ domain_admin_count_sessions_story(Config, Alice, AliceB, _Bob) -> Number = get_ok_value(Path, Res3), ?assertEqual(2, Number). +domain_admin_list_user_sessions_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_list_user_sessions_no_permission_story/2). + +domain_admin_list_user_sessions_no_permission_story(Config, AliceBis) -> + AliceBisJID = escalus_client:full_jid(AliceBis), + Res = list_user_sessions(AliceBisJID, Config), + get_unauthorized(Res). + +domain_admin_count_user_resources_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_count_user_resources_story_no_permission/2). + +domain_admin_count_user_resources_story_no_permission(Config, AliceBis) -> + AliceBisJID = escalus_client:full_jid(AliceBis), + Res = count_user_resources(AliceBisJID, Config), + get_unauthorized(Res). + +domain_admin_get_user_resource_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_get_user_resource_story_no_permission_story/2). + +domain_admin_get_user_resource_story_no_permission_story(Config, AliceBis) -> + AliceBisJID = escalus_client:short_jid(AliceBis), + Res = get_user_resource(AliceBisJID, 2, Config), + get_unauthorized(Res). + +domain_admin_kick_user_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_kick_user_no_permission_story/2). + +domain_admin_kick_user_no_permission_story(Config, AliceBis) -> + AliceBisJID = escalus_client:full_jid(AliceBis), + Reason = <<"Test kick">>, + Res = kick_user(AliceBisJID, Reason, Config), + get_unauthorized(Res). + +domain_admin_set_presence_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_set_presence_no_permission_story/2). + +domain_admin_set_presence_no_permission_story(Config, AliceBis) -> + AliceBisJID = escalus_client:full_jid(AliceBis), + Type = <<"AVAILABLE">>, + Show = <<"ONLINE">>, + Status = <<"Be right back">>, + Priority = 1, + Res = set_presence(AliceBisJID, Type, Show, Status, Priority, Config), + get_unauthorized(Res). + domain_admin_list_users_with_status(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}], fun domain_admin_list_users_with_status_story/3). @@ -183,15 +238,21 @@ domain_admin_list_users_with_status_story(Config, Alice, _AliceB) -> StatusUsers = get_ok_value(Path, Res2), ?assertEqual(1, length(StatusUsers)), check_users([AliceJID], StatusUsers), + % List users with away status for an external domain + Res3 = list_users_with_status(domain_helper:secondary_domain(), AwayStatus, Config), + get_unauthorized(Res3), % List users with dnd status globally escalus_client:send(Alice, DndPresence), - Res3 = list_users_with_status(null, DndStatus, Config), - get_unauthorized(Res3), + Res4 = list_users_with_status(null, DndStatus, Config), + get_unauthorized(Res4), % List users with dnd status for a domain - Res4 = list_users_with_status(domain_helper:domain(), DndStatus, Config), - StatusUsers2 = get_ok_value(Path, Res4), + Res5 = list_users_with_status(domain_helper:domain(), DndStatus, Config), + StatusUsers2 = get_ok_value(Path, Res5), ?assertEqual(1, length(StatusUsers2)), - check_users([AliceJID], StatusUsers2). + check_users([AliceJID], StatusUsers2), + % List users with dnd status for an external domain + Res6 = list_users_with_status(domain_helper:secondary_domain(), AwayStatus, Config), + get_unauthorized(Res6). domain_admin_count_users_with_status(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}], diff --git a/big_tests/tests/graphql_stanza_SUITE.erl b/big_tests/tests/graphql_stanza_SUITE.erl index 6d0912c941e..ede9e54a6be 100644 --- a/big_tests/tests/graphql_stanza_SUITE.erl +++ b/big_tests/tests/graphql_stanza_SUITE.erl @@ -7,7 +7,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(graphql_helper, [execute_user_command/5, execute_command/4, - get_ok_value/2, get_err_code/1, get_err_msg/1, get_coercion_err_msg/1, + get_ok_value/2, get_err_code/1, get_err_msg/1, get_coercion_err_msg/1, get_unauthorized/1]). suite() -> @@ -47,20 +47,13 @@ domain_admin_stanza_cases() -> [admin_send_message, admin_send_message_to_unparsable_jid, admin_send_message_headline, + domain_admin_send_message_no_permission, domain_admin_send_stanza, admin_send_unparsable_stanza, domain_admin_send_stanza_from_unknown_user, - domain_admin_send_stanza_from_unknown_domain] - ++ domain_admin_get_last_messages_cases(). - -domain_admin_get_last_messages_cases() -> - [admin_get_last_messages, - admin_get_last_messages_for_unknown_user, - admin_get_last_messages_with, - admin_get_last_messages_limit, - admin_get_last_messages_limit_enforced, - admin_get_last_messages_before, - domain_admin_get_last_messages_no_permission]. + domain_admin_send_stanza_from_unknown_domain, + domain_admin_get_last_messages_no_permission] + ++ admin_get_last_messages_cases(). user_stanza_cases() -> [user_send_message, @@ -102,8 +95,7 @@ init_per_testcase(CaseName, Config) -> HasMam = proplists:get_value(has_mam, Config, false), MamOnly = lists:member(CaseName, user_get_last_messages_cases() - ++ admin_get_last_messages_cases() - ++ domain_admin_get_last_messages_cases()), + ++ admin_get_last_messages_cases()), case {HasMam, MamOnly} of {false, true} -> {skip, no_mam}; @@ -219,6 +211,16 @@ user_send_message_headline_with_spoofed_from_story(Config, Alice, Bob) -> Res = user_send_message_headline(Alice, From, To, <<"Welcome">>, <<"Hi!">>, Config), spoofed_error(sendMessageHeadLine, Res). +domain_admin_send_message_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}, {bob, 1}], + fun domain_admin_send_message_no_permission_story/3). + +domain_admin_send_message_no_permission_story(Config, AliceBis, Bob) -> + From = escalus_client:full_jid(AliceBis), + To = escalus_client:short_jid(Bob), + Res = send_message(From, To, <<"Hi!">>, Config), + get_unauthorized(Res). + domain_admin_send_stanza(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun domain_admin_send_stanza_story/3). diff --git a/big_tests/tests/graphql_stats_SUITE.erl b/big_tests/tests/graphql_stats_SUITE.erl index 5af5dcbf925..9d17f95335d 100644 --- a/big_tests/tests/graphql_stats_SUITE.erl +++ b/big_tests/tests/graphql_stats_SUITE.erl @@ -30,7 +30,7 @@ admin_stats_tests() -> domain_admin_tests() -> [domain_admin_stats_global_test, - domain_admin_stats_domain_test, + admin_stats_domain_test, domain_admin_stats_domain_no_permission_test]. init_per_suite(Config) -> @@ -107,13 +107,6 @@ admin_stats_domain_with_users_test(Config, _Alice) -> domain_admin_stats_global_test(Config) -> get_unauthorized(get_stats(Config)). -domain_admin_stats_domain_test(Config) -> - Result = get_ok_value([data, stat, domainStats], - get_domain_stats(domain(), Config)), - #{<<"registeredUsers">> := RegisteredUsers, <<"onlineUsers">> := OnlineUsers} = Result, - ?assertEqual(0, RegisteredUsers), - ?assertEqual(0, OnlineUsers). - domain_admin_stats_domain_no_permission_test(Config) -> get_unauthorized(get_domain_stats(secondary_domain(), Config)). diff --git a/big_tests/tests/graphql_token_SUITE.erl b/big_tests/tests/graphql_token_SUITE.erl index 9a95e624545..f3bc902565d 100644 --- a/big_tests/tests/graphql_token_SUITE.erl +++ b/big_tests/tests/graphql_token_SUITE.erl @@ -30,8 +30,8 @@ user_tests() -> domain_admin_tests() -> [admin_request_token_test, - domain_admin_request_token_no_user_test, - domain_admin_revoke_token_no_user_test, + domain_admin_request_token_no_permission_test, + domain_admin_revoke_token_no_permission_test, admin_revoke_token_no_token_test, admin_revoke_token_test]. @@ -119,11 +119,28 @@ user_revoke_token_test(Config, Alice) -> % Domain admin tests -domain_admin_request_token_no_user_test(Config) -> - get_unauthorized(admin_request_token(<<"AAAAA">>, Config)). - -domain_admin_revoke_token_no_user_test(Config) -> - get_unauthorized(admin_revoke_token(<<"AAAAA">>, Config)). +domain_admin_request_token_no_permission_test(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_request_token_no_permission_test/2). + +domain_admin_request_token_no_permission_test(Config, AliceBis) -> + % External domain user + Res = admin_request_token(user_to_bin(AliceBis), Config), + get_unauthorized(Res), + % Non-existing user + Res2 = admin_request_token(<<"AAAAA">>, Config), + get_unauthorized(Res2). + +domain_admin_revoke_token_no_permission_test(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_revoke_token_no_permission_test/2). +domain_admin_revoke_token_no_permission_test(Config, AliceBis) -> + % External domain user + Res = admin_revoke_token(user_to_bin(AliceBis), Config), + get_unauthorized(Res), + % Non-existing user + Res2 = admin_revoke_token(<<"AAAAA">>, Config), + get_unauthorized(Res2). % Admin tests diff --git a/big_tests/tests/graphql_vcard_SUITE.erl b/big_tests/tests/graphql_vcard_SUITE.erl index eb7ec5c9aee..9e894006957 100644 --- a/big_tests/tests/graphql_vcard_SUITE.erl +++ b/big_tests/tests/graphql_vcard_SUITE.erl @@ -42,7 +42,7 @@ domain_admin_vcard_tests() -> admin_get_vcard_no_vcard, domain_admin_get_vcard_no_user, domain_admin_get_vcard_no_permission]. - + admin_vcard_tests() -> [admin_set_vcard, admin_set_vcard_incomplete_fields, @@ -177,9 +177,9 @@ domain_admin_set_vcard_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice_bis, 1}], fun domain_admin_set_vcard_no_permission/2). -domain_admin_set_vcard_no_permission(Config, Alice) -> +domain_admin_set_vcard_no_permission(Config, AliceBis) -> Vcard = complete_vcard_input(), - Result = set_vcard(Vcard, user_to_bin(Alice), Config), + Result = set_vcard(Vcard, user_to_bin(AliceBis), Config), get_unauthorized(Result). %% Admin test cases diff --git a/big_tests/tests/muc_helper.erl b/big_tests/tests/muc_helper.erl index f064220b8bb..d2d7a02abe5 100644 --- a/big_tests/tests/muc_helper.erl +++ b/big_tests/tests/muc_helper.erl @@ -53,8 +53,10 @@ foreach_recipient(Users, VerifyFun) -> end, Users). load_muc() -> + load_muc(domain_helper:host_type()). + +load_muc(HostType) -> Backend = muc_backend(), - HostType = domain_helper:host_type(), MucHostPattern = ct:get_config({hosts, mim, muc_service_pattern}), ct:log("Starting MUC for ~p", [HostType]), Opts = #{host => subhost_pattern(MucHostPattern), backend => Backend, diff --git a/priv/graphql/schemas/admin/session.gql b/priv/graphql/schemas/admin/session.gql index 690dd7b6cf5..3e6724c97d5 100644 --- a/priv/graphql/schemas/admin/session.gql +++ b/priv/graphql/schemas/admin/session.gql @@ -6,7 +6,7 @@ type SessionAdminQuery @protected{ listSessions(domain: String): [Session!] @protected(type: DOMAIN, args: ["domain"]) "Get the number of established sessions for a specified domain or globally" - countSessions(domain: String): Int + countSessions(domain: String): Int @protected(type: DOMAIN, args: ["domain"]) "Get information about all sessions of a user" listUserSessions(user: JID!): [Session!] From b235735e38e5f71910256b840a277d5b073ea563 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 20 Sep 2022 10:19:09 +0200 Subject: [PATCH 049/117] Simplify having an extra module for a very simple function --- src/ejabberd_service.erl | 4 ++-- src/mod_bosh.erl | 2 +- src/mod_muc.erl | 5 ++--- src/mod_roster.erl | 4 ++-- src/mongoose_bin.erl | 4 ++++ src/sha.erl | 31 ------------------------------- 6 files changed, 11 insertions(+), 39 deletions(-) delete mode 100644 src/sha.erl diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 72956d29e62..dd0a71bd99d 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -238,8 +238,8 @@ wait_for_handshake({xmlstreamelement, El}, StateData) -> #xmlel{name = Name, children = Els} = El, case {Name, xml:get_cdata(Els)} of {<<"handshake">>, Digest} -> - case sha:sha1_hex(StateData#state.streamid ++ - StateData#state.password) of + case mongoose_bin:encode_crypto([StateData#state.streamid, + StateData#state.password]) of Digest -> try_register_routes(StateData); _ -> diff --git a/src/mod_bosh.erl b/src/mod_bosh.erl index b667b71ec72..271e7754928 100644 --- a/src/mod_bosh.erl +++ b/src/mod_bosh.erl @@ -361,7 +361,7 @@ store_session(Sid, Socket) -> -spec make_sid() -> binary(). make_sid() -> - sha:sha1_hex(term_to_binary(make_ref())). + mongoose_bin:encode_crypto(term_to_binary(make_ref())). %%-------------------------------------------------------------------- %% HTTP errors diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 89e45be22af..4174a1cc70f 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -1078,9 +1078,8 @@ xfield(Type, Label, Var, Val, Lang) -> %% http://xmpp.org/extensions/xep-0045.html#createroom-unique -spec iq_get_unique(jid:jid()) -> jlib:xmlcdata(). iq_get_unique(From) -> - #xmlcdata{content = sha:sha1_hex(term_to_binary([From, erlang:unique_integer(), - mongoose_bin:gen_from_crypto()]))}. - + Raw = [From, erlang:unique_integer(), mongoose_bin:gen_from_crypto()], + #xmlcdata{content = mongoose_bin:encode_crypto(term_to_binary(Raw))}. -spec iq_get_register_info(host_type(), jid:server(), jid:simple_jid() | jid:jid(), ejabberd:lang()) diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 100bb67466d..6e35d8bc7f4 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -214,7 +214,7 @@ process_local_iq(Acc, From, To, #iq{type = Type} = IQ) -> roster_hash(Items) -> L = [R#roster{groups = lists:sort(Grs)} || R = #roster{groups = Grs} <- Items], - sha:sha1_hex(term_to_binary(lists:sort(L))). + mongoose_bin:encode_crypto(term_to_binary(lists:sort(L))). -spec roster_versioning_enabled(mongooseim:host_type()) -> boolean(). roster_versioning_enabled(HostType) -> @@ -997,7 +997,7 @@ write_roster_version_t(HostType, LUser, LServer) -> -spec write_roster_version(mongooseim:host_type(), jid:luser(), jid:lserver(), transaction_state()) -> version(). write_roster_version(HostType, LUser, LServer, TransactionState) -> - Ver = sha:sha1_hex(term_to_binary(os:timestamp())), + Ver = mongoose_bin:encode_crypto(term_to_binary(os:timestamp())), mod_roster_backend:write_roster_version(HostType, LUser, LServer, TransactionState, Ver), Ver. diff --git a/src/mongoose_bin.erl b/src/mongoose_bin.erl index 94edb097688..db832e55659 100644 --- a/src/mongoose_bin.erl +++ b/src/mongoose_bin.erl @@ -10,6 +10,7 @@ -export([tokens/2, join/2, + encode_crypto/1, gen_from_crypto/0, gen_from_timestamp/0, string_to_binary/1]). @@ -43,6 +44,9 @@ gen_from_timestamp() -> MicroB = integer_to_binary(Micro), <>. +-spec encode_crypto(iodata()) -> binary(). +encode_crypto(Text) -> base16:encode(crypto:hash(sha, Text)). + -spec string_to_binary(binary() | list()) -> binary(). string_to_binary(S) when is_list(S) -> % If list is in Erlang representation of Unicode, we must use `unicode` module diff --git a/src/sha.erl b/src/sha.erl deleted file mode 100644 index dc9fb8bfa76..00000000000 --- a/src/sha.erl +++ /dev/null @@ -1,31 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : sha.erl -%%% Author : Alexey Shchepin -%%% Purpose : -%%% Created : 20 Dec 2002 by Alexey Shchepin -%%% -%%% -%%% ejabberd, Copyright (C) 2002-2011 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License -%%% along with this program; if not, write to the Free Software -%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -%%% -%%%---------------------------------------------------------------------- - --module(sha). --export([sha1_hex/1]). - --spec sha1_hex(iodata()) -> binary(). -sha1_hex(Text) -> base16:encode(crypto:hash(sha, Text)). - From d4264b296febd57e3fc61fd95817fb2cafc6841a Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 20 Sep 2022 10:55:39 +0200 Subject: [PATCH 050/117] Update (maybe redundant) test suite --- test/{sha_SUITE.erl => mongoose_bin_SUITE.erl} | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) rename test/{sha_SUITE.erl => mongoose_bin_SUITE.erl} (54%) diff --git a/test/sha_SUITE.erl b/test/mongoose_bin_SUITE.erl similarity index 54% rename from test/sha_SUITE.erl rename to test/mongoose_bin_SUITE.erl index d95d4068942..992e10d4927 100644 --- a/test/sha_SUITE.erl +++ b/test/mongoose_bin_SUITE.erl @@ -1,4 +1,4 @@ --module(sha_SUITE). +-module(mongoose_bin_SUITE). -compile([export_all, nowarn_export_all]). -include_lib("proper/include/proper.hrl"). @@ -10,14 +10,16 @@ all() -> ]. sanity_check(_) -> - %% @doc vis: echo -n "Foo" | sha1sum - <<"201a6b3053cc1422d2c3670b62616221d2290929">> = sha:sha1_hex(<<"Foo">>). + %% @doc vis: echo -n "Foo" | sha256sum + Encoded = mongoose_bin:encode_crypto(<<"Foo">>), + <<"201a6b3053cc1422d2c3670b62616221d2290929">> = Encoded. always_produces_well_formed_output(_) -> prop(always_produces_well_formed_output, ?FORALL(BinaryBlob, binary(), - true == is_well_formed(sha:sha1_hex(BinaryBlob)))). + true == is_well_formed(mongoose_bin:encode_crypto(BinaryBlob)))). is_well_formed(Binary) -> - 40 == size(Binary) andalso + true =:= is_binary(Binary) andalso + 40 =:= byte_size(Binary) andalso nomatch == re:run(Binary, "[^0-9a-f]"). From efcb40d4f15b7b99cc6cb8125ae272c30ed92344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Fri, 16 Sep 2022 10:51:26 +0200 Subject: [PATCH 051/117] Refactored hook handlers in ejabberd_sm module --- src/ejabberd_sm.erl | 104 +++++++++++++++++++------------------ src/mongoose_hooks.erl | 23 +++++--- test/ejabberd_sm_SUITE.erl | 1 + 3 files changed, 71 insertions(+), 57 deletions(-) diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index a27087d8c3d..f29cb910090 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -40,9 +40,6 @@ store_info/3, get_info/2, remove_info/2, - check_in_subscription/5, - bounce_offline_message/4, - disconnect_removed_user/3, get_user_resources/1, set_presence/6, unset_presence/5, @@ -70,7 +67,11 @@ ]). %% Hook handlers --export([node_cleanup/2]). +-export([node_cleanup/3, + check_in_subscription/3, + bounce_offline_message/3, + disconnect_removed_user/3 + ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -80,10 +81,8 @@ -export([do_filter/3]). -export([do_route/4]). --ignore_xref([bounce_offline_message/4, check_in_subscription/5, disconnect_removed_user/3, - do_filter/3, do_route/4, force_update_presence/2, get_unique_sessions_number/0, - get_user_present_pids/2, node_cleanup/2, start_link/0, user_resources/2, - sm_backend/0]). +-ignore_xref([do_filter/3, do_route/4, force_update_presence/2, get_unique_sessions_number/0, + get_user_present_pids/2, start_link/0, user_resources/2, sm_backend/0]). -include("mongoose.hrl"). -include("jlib.hrl"). @@ -294,40 +293,6 @@ remove_info(JID, Key) -> end end. --spec check_in_subscription(Acc, ToJID, FromJID, Type, Reason) -> any() | {stop, false} when - Acc :: any(), - ToJID :: jid:jid(), - FromJID :: jid:jid(), - Type :: any(), - Reason :: any(). -check_in_subscription(Acc, ToJID, _FromJID, _Type, _Reason) -> - case ejabberd_auth:does_user_exist(ToJID) of - true -> - Acc; - false -> - {stop, mongoose_acc:set(hook, result, false, Acc)} - end. - --spec bounce_offline_message(Acc, From, To, Packet) -> {stop, Acc} when - Acc :: map(), - From :: jid:jid(), - To :: jid:jid(), - Packet :: exml:element(). -bounce_offline_message(Acc, From, To, Packet) -> - Acc1 = mongoose_hooks:xmpp_bounce_message(Acc), - E = mongoose_xmpp_errors:service_unavailable(<<"en">>, <<"Bounce offline message">>), - {Acc2, Err} = jlib:make_error_reply(Acc1, Packet, E), - Acc3 = ejabberd_router:route(To, From, Acc2, Err), - {stop, Acc3}. - --spec disconnect_removed_user(mongoose_acc:t(), User :: jid:user(), - Server :: jid:server()) -> mongoose_acc:t(). -disconnect_removed_user(Acc, User, Server) -> - lists:map(fun({_, Pid}) -> ejabberd_c2s:terminate_session(Pid, <<"User removed">>) end, - get_user_present_pids(User, Server)), - Acc. - - -spec get_user_resources(JID :: jid:jid()) -> [binary()]. get_user_resources(JID) -> #jid{luser = LUser, lserver = LServer} = JID, @@ -473,10 +438,46 @@ run_session_cleanup_hook(#session{usr = {U, S, R}, sid = SID}) -> %% Hook handlers %%==================================================================== -node_cleanup(Acc, Node) -> +-spec node_cleanup(Acc, Args, Extra) -> {ok, Acc} when + Acc :: any(), + Args :: #{node := node()}, + Extra :: map(). +node_cleanup(Acc, #{node := Node}, _) -> Timeout = timer:minutes(1), Res = gen_server:call(?MODULE, {node_cleanup, Node}, Timeout), - maps:put(?MODULE, Res, Acc). + {ok, maps:put(?MODULE, Res, Acc)}. + +-spec check_in_subscription(Acc, Args, Extra)-> {ok, Acc} | {stop, false} when + Acc :: any(), + Args :: #{to_jid := jid:jid()}, + Extra :: map(). +check_in_subscription(Acc, #{to_jid := ToJID}, _) -> + case ejabberd_auth:does_user_exist(ToJID) of + true -> + {ok, Acc}; + false -> + {stop, mongoose_acc:set(hook, result, false, Acc)} + end. + +-spec bounce_offline_message(Acc, Args, Extra) -> {stop, Acc} when + Acc :: map(), + Args :: #{from := jid:jid(), to := jid:jid(), packet := exml:element()}, + Extra :: map(). +bounce_offline_message(Acc, #{from := From, to := To, packet := Packet}, _) -> + Acc1 = mongoose_hooks:xmpp_bounce_message(Acc), + E = mongoose_xmpp_errors:service_unavailable(<<"en">>, <<"Bounce offline message">>), + {Acc2, Err} = jlib:make_error_reply(Acc1, Packet, E), + Acc3 = ejabberd_router:route(To, From, Acc2, Err), + {stop, Acc3}. + +-spec disconnect_removed_user(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: #{user := jid:user(), server := jid:server()}, + Extra :: map(). +disconnect_removed_user(Acc, #{user := User, server := Server}, _) -> + lists:map(fun({_, Pid}) -> ejabberd_c2s:terminate_session(Pid, <<"User removed">>) end, + get_user_present_pids(User, Server)), + {ok, Acc}. %%==================================================================== %% gen_server callbacks @@ -495,18 +496,19 @@ init([]) -> ejabberd_sm_backend:init(#{backend => Backend}), ets:new(sm_iqtable, [named_table, protected, {read_concurrency, true}]), - ejabberd_hooks:add(node_cleanup, global, ?MODULE, node_cleanup, 50), - lists:foreach(fun(HostType) -> ejabberd_hooks:add(hooks(HostType)) end, + gen_hook:add_handler(node_cleanup, global, fun ?MODULE:node_cleanup/3, #{}, 50), + lists:foreach(fun(HostType) -> gen_hook:add_handlers(hooks(HostType)) end, ?ALL_HOST_TYPES), ejabberd_commands:register_commands(commands()), {ok, #state{}}. +-spec hooks(binary()) -> [gen_hook:hook_tuple()]. hooks(HostType) -> [ - {roster_in_subscription, HostType, ejabberd_sm, check_in_subscription, 20}, - {offline_message_hook, HostType, ejabberd_sm, bounce_offline_message, 100}, - {offline_groupchat_message_hook, HostType, ejabberd_sm, bounce_offline_message, 100}, - {remove_user, HostType, ejabberd_sm, disconnect_removed_user, 100} + {roster_in_subscription, HostType, fun ?MODULE:check_in_subscription/3, #{}, 20}, + {offline_message_hook, HostType, fun ?MODULE:bounce_offline_message/3, #{}, 100}, + {offline_groupchat_message_hook, HostType, fun ?MODULE:bounce_offline_message/3, #{}, 100}, + {remove_user, HostType, fun ?MODULE:disconnect_removed_user/3, #{}, 100} ]. %%-------------------------------------------------------------------- @@ -835,7 +837,7 @@ route_message_by_type(<<"error">>, _From, _To, Acc, _Packet) -> route_message_by_type(<<"groupchat">>, From, To, Acc, Packet) -> mongoose_hooks:offline_groupchat_message_hook(Acc, From, To, Packet); route_message_by_type(<<"headline">>, From, To, Acc, Packet) -> - {stop, Acc1} = bounce_offline_message(Acc, From, To, Packet), + {stop, Acc1} = bounce_offline_message(Acc, #{from => From, to => To, packet => Packet}, #{}), Acc1; route_message_by_type(_, From, To, Acc, Packet) -> HostType = mongoose_acc:host_type(Acc), diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 959588183ec..e0e903ac52d 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -368,8 +368,11 @@ register_user(HostType, LServer, LUser) -> LUser :: jid:luser(), Result :: mongoose_acc:t(). remove_user(Acc, LServer, LUser) -> + Params = #{user => LUser, server => LServer}, + Args = [LUser, LServer], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(remove_user, HostType, Acc, [LUser, LServer]). + run_hook_for_host_type(remove_user, HostType, Acc, ParamsWithLegacyArgs). -spec resend_offline_messages_hook(Acc, JID) -> Result when Acc :: mongoose_acc:t(), @@ -690,9 +693,11 @@ privacy_updated_list(HostType, OldList, NewList) -> Packet :: exml:element(), Result :: mongoose_acc:t(). offline_groupchat_message_hook(Acc, From, To, Packet) -> + Params = #{from => From, to => To, packet => Packet}, + Args = [From, To, Packet], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(offline_groupchat_message_hook, HostType, Acc, - [From, To, Packet]). + run_hook_for_host_type(offline_groupchat_message_hook, HostType, Acc, ParamsWithLegacyArgs). -spec offline_message_hook(Acc, From, To, Packet) -> Result when Acc :: mongoose_acc:t(), @@ -701,8 +706,11 @@ offline_groupchat_message_hook(Acc, From, To, Packet) -> Packet :: exml:element(), Result :: mongoose_acc:t(). offline_message_hook(Acc, From, To, Packet) -> + Params = #{from => From, to => To, packet => Packet}, + Args = [From, To, Packet], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(offline_message_hook, HostType, Acc, [From, To, Packet]). + run_hook_for_host_type(offline_message_hook, HostType, Acc, ParamsWithLegacyArgs). -spec set_presence_hook(Acc, JID, Presence) -> Result when Acc :: mongoose_acc:t(), @@ -841,9 +849,12 @@ roster_get_versioning_feature(HostType) -> Reason :: any(), Result :: mongoose_acc:t(). roster_in_subscription(Acc, To, From, Type, Reason) -> + ToJID = jid:to_bare(To), + Params = #{to_jid => ToJID, from => From, type => Type, reason => Reason}, + Args = [ToJID, From, Type, Reason], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(roster_in_subscription, HostType, Acc, - [jid:to_bare(To), From, Type, Reason]). + run_hook_for_host_type(roster_in_subscription, HostType, Acc, ParamsWithLegacyArgs). %%% @doc The `roster_out_subscription' hook is called %%% when a user sends out subscription. diff --git a/test/ejabberd_sm_SUITE.erl b/test/ejabberd_sm_SUITE.erl index fd09010654d..aa85adcfa87 100644 --- a/test/ejabberd_sm_SUITE.erl +++ b/test/ejabberd_sm_SUITE.erl @@ -608,6 +608,7 @@ sm_backend(ejabberd_sm_mnesia) -> mnesia. set_meck() -> meck:expect(gen_hook, add_handler, fun(_, _, _, _, _) -> ok end), + meck:expect(gen_hook, add_handlers, fun(_) -> ok end), meck:new(ejabberd_commands, []), meck:expect(ejabberd_commands, register_commands, fun(_) -> ok end), meck:expect(ejabberd_commands, unregister_commands, fun(_) -> ok end), From 0ba7a50a106e63a42468e71e8b145d2bfc7c9a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Tue, 20 Sep 2022 14:35:18 +0200 Subject: [PATCH 052/117] Changed params in remove_user hook from luser, lserver to jid --- src/ejabberd_sm.erl | 4 ++-- src/mongoose_hooks.erl | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index f29cb910090..3a3f92a27ea 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -472,9 +472,9 @@ bounce_offline_message(Acc, #{from := From, to := To, packet := Packet}, _) -> -spec disconnect_removed_user(Acc, Args, Extra) -> {ok, Acc} when Acc :: mongoose_acc:t(), - Args :: #{user := jid:user(), server := jid:server()}, + Args :: #{jid := jid:jid()}, Extra :: map(). -disconnect_removed_user(Acc, #{user := User, server := Server}, _) -> +disconnect_removed_user(Acc, #{jid := #jid{luser = User, lserver = Server}}, _) -> lists:map(fun({_, Pid}) -> ejabberd_c2s:terminate_session(Pid, <<"User removed">>) end, get_user_present_pids(User, Server)), {ok, Acc}. diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index e0e903ac52d..4c876d4aba5 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -368,7 +368,8 @@ register_user(HostType, LServer, LUser) -> LUser :: jid:luser(), Result :: mongoose_acc:t(). remove_user(Acc, LServer, LUser) -> - Params = #{user => LUser, server => LServer}, + Jid = jid:make_bare(LUser, LServer), + Params = #{jid => Jid}, Args = [LUser, LServer], ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), From bf99f2ae66a51b487e102503fac972b8f849ec34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 21 Sep 2022 08:29:28 +0200 Subject: [PATCH 053/117] Include overlay vars from vars-toml.config This way there is only one overlay file, and the overlays work correctly with the latest Rebar3. As we do some templating in our tools, they need to support file inclusion as well. --- big_tests/run_common_test.erl | 25 +++++++++++++------- big_tests/tests/ejabberd_node_utils.erl | 31 ++++++++++++------------- rebar.config | 10 ++++---- rel/fed1.vars-toml.config | 2 ++ rel/mim1.vars-toml.config | 2 ++ rel/mim2.vars-toml.config | 2 ++ rel/mim3.vars-toml.config | 2 ++ rel/reg1.vars-toml.config | 2 ++ tools/test_runner/apply_templates.erl | 19 ++++++++------- 9 files changed, 57 insertions(+), 38 deletions(-) diff --git a/big_tests/run_common_test.erl b/big_tests/run_common_test.erl index 97bc316eb1c..b3b6dc6d917 100644 --- a/big_tests/run_common_test.erl +++ b/big_tests/run_common_test.erl @@ -278,22 +278,20 @@ is_test_host_enabled(HostName) -> enable_preset_on_node(Node, PresetVars, HostVarsFilePrefix) -> {ok, Cwd} = call(Node, file, get_cwd, []), TemplatePath = filename:join([repo_dir(), "rel", "files", "mongooseim.toml"]), - DefaultVarsPath = filename:join([repo_dir(), "rel", "vars-toml.config"]), NodeVarsPath = filename:join([repo_dir(), "rel", HostVarsFilePrefix ++ ".vars-toml.config"]), {ok, Template} = handle_file_error(TemplatePath, file:read_file(TemplatePath)), - {ok, DefaultVars} = handle_file_error(DefaultVarsPath, file:consult(DefaultVarsPath)), - {ok, NodeVars} = handle_file_error(NodeVarsPath, file:consult(NodeVarsPath)), + NodeVars = read_vars(NodeVarsPath), - TemplatedConfig = template_config(Template, [DefaultVars, NodeVars, PresetVars]), + TemplatedConfig = template_config(Template, NodeVars ++ PresetVars), CfgPath = filename:join([Cwd, "etc", "mongooseim.toml"]), ok = call(Node, file, write_file, [CfgPath, TemplatedConfig]), call(Node, application, stop, [mongooseim]), call(Node, application, start, [mongooseim]), ok. -template_config(Template, Vars) -> - MergedVars = ensure_binary_strings(merge_vars(Vars)), +template_config(Template, RawVars) -> + MergedVars = ensure_binary_strings(maps:from_list(RawVars)), %% Render twice to replace variables in variables Tmp = bbmustache:render(Template, MergedVars, [{key_type, atom}]), bbmustache:render(Tmp, MergedVars, [{key_type, atom}]). @@ -305,11 +303,20 @@ merge_vars([Vars1, Vars2|Rest]) -> merge_vars([Vars|Rest]); merge_vars([Vars]) -> Vars. +read_vars(File) -> + {ok, Terms} = handle_file_error(File, file:consult(File)), + lists:flatmap(fun({Key, Val}) -> + [{Key, Val}]; + (IncludedFile) when is_list(IncludedFile) -> + Path = filename:join(filename:dirname(File), IncludedFile), + read_vars(Path) + end, Terms). + %% bbmustache tries to iterate over lists, so we need to make them binaries ensure_binary_strings(Vars) -> - lists:map(fun({dbs, V}) -> {dbs, V}; - ({K, V}) when is_list(V) -> {K, list_to_binary(V)}; - ({K, V}) -> {K, V} + maps:map(fun(dbs, V) -> V; + (_K, V) when is_list(V) -> list_to_binary(V); + (_K, V) -> V end, Vars). call(Node, M, F, A) -> diff --git a/big_tests/tests/ejabberd_node_utils.erl b/big_tests/tests/ejabberd_node_utils.erl index eb2b5a1fcb2..b3aa0432ef5 100644 --- a/big_tests/tests/ejabberd_node_utils.erl +++ b/big_tests/tests/ejabberd_node_utils.erl @@ -154,41 +154,40 @@ modify_config_file(CfgVarsToChange, Config) -> ConfigVariable :: atom(), Value :: string(). modify_config_file(Host, VarsToChange, Config, Format) -> - VarsFile = vars_file(Format), NodeVarsFile = ct:get_config({hosts, Host, vars}, Config) ++ "." ++ vars_file(Format), TemplatePath = config_template_path(Config, Format), - DefaultVarsPath = config_vars_path(VarsFile, Config), NodeVarsPath = config_vars_path(NodeVarsFile, Config), {ok, Template} = file:read_file(TemplatePath), - {ok, DefaultVars} = file:consult(DefaultVarsPath), - {ok, NodeVars} = file:consult(NodeVarsPath), + NodeVars = read_vars(NodeVarsPath), PresetVars = preset_vars(Config, Format), - TemplatedConfig = template_config(Template, [DefaultVars, NodeVars, PresetVars, VarsToChange]), + TemplatedConfig = template_config(Template, NodeVars ++ PresetVars ++ VarsToChange), RPCSpec = distributed_helper:Host(), NewCfgPath = update_config_path(RPCSpec, Format), ok = ejabberd_node_utils:call_fun(RPCSpec, file, write_file, [NewCfgPath, TemplatedConfig]). +read_vars(File) -> + {ok, Terms} = file:consult(File), + lists:flatmap(fun({Key, Val}) -> + [{Key, Val}]; + (IncludedFile) when is_list(IncludedFile) -> + Path = filename:join(filename:dirname(File), IncludedFile), + read_vars(Path) + end, Terms). + template_config(Template, Vars) -> - MergedVars = ensure_binary_strings(merge_vars(Vars)), + MergedVars = ensure_binary_strings(maps:from_list(Vars)), %% Render twice to replace variables in variables Tmp = bbmustache:render(Template, MergedVars, [{key_type, atom}]), bbmustache:render(Tmp, MergedVars, [{key_type, atom}]). -merge_vars([Vars1, Vars2|Rest]) -> - Vars = lists:foldl(fun ({Var, Val}, Acc) -> - lists:keystore(Var, 1, Acc, {Var, Val}) - end, Vars1, Vars2), - merge_vars([Vars|Rest]); -merge_vars([Vars]) -> Vars. - %% bbmustache tries to iterate over lists, so we need to make them binaries ensure_binary_strings(Vars) -> - lists:map(fun({dbs, V}) -> {dbs, V}; - ({K, V}) when is_list(V) -> {K, list_to_binary(V)}; - ({K, V}) -> {K, V} + maps:map(fun(dbs, V) -> V; + (_K, V) when is_list(V) -> list_to_binary(V); + (_K, V) -> V end, Vars). update_config_path(RPCSpec, Format) -> diff --git a/rebar.config b/rebar.config index 2bca5f53518..f1300c1ad57 100644 --- a/rebar.config +++ b/rebar.config @@ -164,15 +164,15 @@ {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}, {erl_opts, [{d, 'PROD_NODE'}]} ]}, %% development nodes - {mim1, [{relx, [ {overlay_vars, ["rel/vars-toml.config", "rel/mim1.vars-toml.config"]}, + {mim1, [{relx, [ {overlay_vars, "rel/mim1.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}]}, - {mim2, [{relx, [ {overlay_vars, ["rel/vars-toml.config", "rel/mim2.vars-toml.config"]}, + {mim2, [{relx, [ {overlay_vars, "rel/mim2.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}]}, - {mim3, [{relx, [ {overlay_vars, ["rel/vars-toml.config", "rel/mim3.vars-toml.config"]}, + {mim3, [{relx, [ {overlay_vars, "rel/mim3.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}]}, - {fed1, [{relx, [ {overlay_vars, ["rel/vars-toml.config", "rel/fed1.vars-toml.config"]}, + {fed1, [{relx, [ {overlay_vars, "rel/fed1.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}]}, - {reg1, [{relx, [ {overlay_vars, ["rel/vars-toml.config", "rel/reg1.vars-toml.config"]}, + {reg1, [{relx, [ {overlay_vars, "rel/reg1.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}]}, {test, [{extra_src_dirs, [{"test", [{recursive, true}]}]}]} ]}. diff --git a/rel/fed1.vars-toml.config b/rel/fed1.vars-toml.config index 8db4c7981db..78ece4941a5 100644 --- a/rel/fed1.vars-toml.config +++ b/rel/fed1.vars-toml.config @@ -1,3 +1,5 @@ +"./vars-toml.config". + {node_name, "fed1@localhost"}. {c2s_port, 5242}. diff --git a/rel/mim1.vars-toml.config b/rel/mim1.vars-toml.config index 2db63aee1d9..f85a780b024 100644 --- a/rel/mim1.vars-toml.config +++ b/rel/mim1.vars-toml.config @@ -1,3 +1,5 @@ +"./vars-toml.config". + {c2s_tls_port, 5223}. {outgoing_s2s_port, 5299}. {service_port, 8888}. diff --git a/rel/mim2.vars-toml.config b/rel/mim2.vars-toml.config index a1a08d2dc10..89c144e4500 100644 --- a/rel/mim2.vars-toml.config +++ b/rel/mim2.vars-toml.config @@ -1,3 +1,5 @@ +"./vars-toml.config". + {node_name, "ejabberd2@localhost"}. {c2s_port, 5232}. diff --git a/rel/mim3.vars-toml.config b/rel/mim3.vars-toml.config index 422d2de57fd..0687b7deac9 100644 --- a/rel/mim3.vars-toml.config +++ b/rel/mim3.vars-toml.config @@ -1,3 +1,5 @@ +"./vars-toml.config". + {node_name, "mongooseim3@localhost"}. {c2s_port, 5262}. diff --git a/rel/reg1.vars-toml.config b/rel/reg1.vars-toml.config index 8281f21720c..ac14930954f 100644 --- a/rel/reg1.vars-toml.config +++ b/rel/reg1.vars-toml.config @@ -1,3 +1,5 @@ +"./vars-toml.config". + {node_name, "reg1@localhost"}. {c2s_port, 5252}. diff --git a/tools/test_runner/apply_templates.erl b/tools/test_runner/apply_templates.erl index 382a21be719..cef57a23d42 100644 --- a/tools/test_runner/apply_templates.erl +++ b/tools/test_runner/apply_templates.erl @@ -20,10 +20,17 @@ main([NodeAtom, BuildDirAtom]) -> overlay_vars(Node) -> - Vars = consult_map("rel/vars-toml.config"), - NodeVars = consult_map("rel/" ++ atom_to_list(Node) ++ ".vars-toml.config"), - %% NodeVars overrides Vars - ensure_binary_strings(maps:merge(Vars, NodeVars)). + File = "rel/" ++ atom_to_list(Node) ++ ".vars-toml.config", + ensure_binary_strings(maps:from_list(read_vars(File))). + +read_vars(File) -> + {ok, Terms} = file:consult(File), + lists:flatmap(fun({Key, Val}) -> + [{Key, Val}]; + (IncludedFile) when is_list(IncludedFile) -> + Path = filename:join(filename:dirname(File), IncludedFile), + read_vars(Path) + end, Terms). %% bbmustache tries to iterate over lists, so we need to make them binaries ensure_binary_strings(Vars) -> @@ -31,10 +38,6 @@ ensure_binary_strings(Vars) -> (_K, V) -> V end, Vars). -consult_map(File) -> - {ok, Vars} = file:consult(File), - maps:from_list(Vars). - %% Based on rebar.config overlay section templates(RelDir) -> simple_templates(RelDir) ++ erts_templates(RelDir). From efee290620055c693240c2c6bf13e36c3a519b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 21 Sep 2022 08:51:49 +0200 Subject: [PATCH 054/117] Get rid of rel/vars-toml.config.in It is easier to modify vars-toml.config directly --- Makefile | 6 +----- rel/{vars-toml.config.in => vars-toml.config} | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) rename rel/{vars-toml.config.in => vars-toml.config} (96%) diff --git a/Makefile b/Makefile index 38608568f39..029fe7bea81 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,6 @@ clean: -rm -rf asngen -rm configure.out -rm rel/configure.vars.config - -rm rel/vars-toml.config # REBAR_CT_EXTRA_ARGS comes from a test runner ct: @@ -27,7 +26,7 @@ ct: eunit: @$(RUN) $(REBAR) eunit -rel: certs configure.out rel/vars-toml.config +rel: certs configure.out rel/configure.vars.config . ./configure.out && $(REBAR) as prod release shell: certs etc/mongooseim.cfg @@ -40,9 +39,6 @@ rock: elif [ "$(BRANCH)" ]; then tools/rock_changed.sh $(BRANCH); \ else tools/rock_changed.sh; fi -rel/vars-toml.config: rel/vars-toml.config.in rel/configure.vars.config - cat $^ > $@ - ## Don't allow these files to go out of sync! configure.out rel/configure.vars.config: ./tools/configure with-all without-jingle-sip diff --git a/rel/vars-toml.config.in b/rel/vars-toml.config similarity index 96% rename from rel/vars-toml.config.in rename to rel/vars-toml.config index 62b2d59b2e0..8e8d91d8d5c 100644 --- a/rel/vars-toml.config.in +++ b/rel/vars-toml.config @@ -55,7 +55,9 @@ {s2s_certfile, "\"priv/ssl/fake_server.pem\""}. {all_metrics_are_global, "false"}. -%% Defined in Makefile by appending configure.vars.config +"./configure.vars.config". + +%% Defined by appending configure.vars.config %% Uncomment for manual release generation. %{mongooseim_runner_user, ""}. %{mongooseim_script_dir, "$(cd ${0%/*} && pwd)"}. From c30fd94dbff59df501c00088213b046b5ccea530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 08:59:21 +0200 Subject: [PATCH 055/117] Fail if creds are provided, but not configured This way we don't give the false impression of a password being checked. It already works like this for mongoose_domain_handler. --- big_tests/tests/rest_SUITE.erl | 15 ++++++++------- src/mongoose_admin_api/mongoose_admin_api.erl | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/big_tests/tests/rest_SUITE.erl b/big_tests/tests/rest_SUITE.erl index a0fb556e0d7..811048981a0 100644 --- a/big_tests/tests/rest_SUITE.erl +++ b/big_tests/tests/rest_SUITE.erl @@ -66,7 +66,8 @@ auth_test_cases() -> auth_fails_incorrect_creds]. blank_auth_testcases() -> - [auth_always_passes_blank_creds]. + [auth_passes_without_creds, + auth_fails_with_creds]. test_cases() -> [non_existent_command_returns404, @@ -153,13 +154,13 @@ auth_fails_incorrect_creds(_Config) -> % try to login with different creds {?NOT_AUTHORIZED, _} = gett(admin, path("users", [domain()]), {<<"ola">>, <<"mapsa">>}). -auth_always_passes_blank_creds(_Config) -> - % we set control creds for blank - rest_helper:change_admin_creds(any), - % try with any auth - {?OK, Users} = gett(admin, path("users", [domain()]), {<<"aaaa">>, <<"bbbb">>}), +auth_passes_without_creds(_Config) -> % try with no auth - {?OK, Users} = gett(admin, path("users", [domain()])). + {?OK, _Users} = gett(admin, path("users", [domain()])). + +auth_fails_with_creds(_Config) -> + % try with any auth + {?NOT_AUTHORIZED, _} = gett(admin, path("users", [domain()]), {<<"aaaa">>, <<"bbbb">>}). non_existent_command_returns404(_C) -> {?NOT_FOUND, _} = gett(admin, <<"/isitthereornot">>). diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index 36e103c7eca..6f75a168d56 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -101,8 +101,8 @@ authorize(#{username := Username, password := Password}, AuthDetails) -> _ -> false end; -authorize(#{}, _) -> - true. % no credentials required +authorize(#{}, AuthDetails) -> + AuthDetails =:= undefined. % Do not accept basic auth when not configured -spec parse_body(req()) -> #{atom() => jiffy:json_value()}. parse_body(Req) -> From 065dafe7e3c8accd3d118af73e116ad6734436f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 09:01:23 +0200 Subject: [PATCH 056/117] Integrate the domain handler with mongoose_admin_api Error messages are changed to capitalized strings, because it is the convention used for the whole admin API. Error codes are left as they were except ones that are already provided by mongoose_admin_api itself, e.g. "401 Unauthorized". The error messages should be common for REST and GraphQL, but this unification is left for the future. --- src/domain/mongoose_domain_handler.erl | 220 ------------------ src/mongoose_admin_api/mongoose_admin_api.erl | 7 +- .../mongoose_admin_api_domain.erl | 150 ++++++++++++ src/mongoose_http_handler.erl | 1 - .../mongoose_system_metrics_collector.erl | 2 +- 5 files changed, 156 insertions(+), 224 deletions(-) delete mode 100644 src/domain/mongoose_domain_handler.erl create mode 100644 src/mongoose_admin_api/mongoose_admin_api_domain.erl diff --git a/src/domain/mongoose_domain_handler.erl b/src/domain/mongoose_domain_handler.erl deleted file mode 100644 index 9851a668141..00000000000 --- a/src/domain/mongoose_domain_handler.erl +++ /dev/null @@ -1,220 +0,0 @@ -%% REST API for domain actions. --module(mongoose_domain_handler). - --behaviour(mongoose_http_handler). --behaviour(cowboy_rest). - -%% mongoose_http_handler callbacks --export([config_spec/0, routes/1]). - -%% config processing callbacks --export([process_config/1]). - -%% Standard cowboy_rest callbacks. --export([init/2, - allowed_methods/2, - content_types_accepted/2, - content_types_provided/2, - is_authorized/2, - delete_resource/2]). - -%% Custom cowboy_rest callbacks. --export([handle_domain/2, - to_json/2]). - --ignore_xref([cowboy_router_paths/2, handle_domain/2, to_json/2]). - --include("mongoose_logger.hrl"). --include("mongoose_config_spec.hrl"). --type state() :: map(). - --type handler_options() :: #{path := string(), username => binary(), password => binary(), - atom() => any()}. - -%% mongoose_http_handler callbacks - --spec config_spec() -> mongoose_config_spec:config_section(). -config_spec() -> - #section{items = #{<<"username">> => #option{type = binary}, - <<"password">> => #option{type = binary}}, - process = fun ?MODULE:process_config/1}. - -process_config(Opts) -> - case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of - true -> - Opts; - false -> - error(#{what => both_username_and_password_required, opts => Opts}) - end. - --spec routes(handler_options()) -> mongoose_http_handler:routes(). -routes(Opts = #{path := BasePath}) -> - [{[BasePath, "/domains/:domain"], ?MODULE, Opts}]. - -%% cowboy_rest callbacks - -init(Req, Opts) -> - {cowboy_rest, Req, Opts}. - -allowed_methods(Req, State) -> - {[<<"GET">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>], Req, State}. - -content_types_accepted(Req, State) -> - {[{{<<"application">>, <<"json">>, '*'}, handle_domain}], - Req, State}. - -content_types_provided(Req, State) -> - {[{{<<"application">>, <<"json">>, '*'}, to_json}], Req, State}. - -is_authorized(Req, State) -> - HeaderDetails = cowboy_req:parse_header(<<"authorization">>, Req), - ConfigDetails = state_to_details(State), - case check_auth(HeaderDetails, ConfigDetails) of - ok -> - {true, Req, State}; - {error, auth_header_passed_but_not_expected} -> - {false, reply_error(403, <<"basic auth provided, but not configured">>, Req), State}; - {error, auth_password_invalid} -> - {false, reply_error(403, <<"basic auth provided, invalid password">>, Req), State}; - {error, no_basic_auth_provided} -> - {false, reply_error(403, <<"basic auth is required">>, Req), State} - end. - -state_to_details(#{username := User, password := Pass}) -> - {basic, User, Pass}; -state_to_details(_) -> - not_configured. - -check_auth({basic, _User, _Pass}, _ConfigDetails = not_configured) -> - {error, auth_header_passed_but_not_expected}; -check_auth(_HeaderDetails, _ConfigDetails = not_configured) -> - ok; -check_auth({basic, User, Pass}, {basic, User, Pass}) -> - ok; -check_auth({basic, _, _}, {basic, _, _}) -> - {error, auth_password_invalid}; -check_auth(_, {basic, _, _}) -> - {error, no_basic_auth_provided}. - -%% Custom cowboy_rest callbacks: --spec to_json(Req, State) -> {Body, Req, State} | {stop, Req, State} - when Req :: cowboy_req:req(), State :: state(), Body :: binary(). -to_json(Req, State) -> - ExtDomain = cowboy_req:binding(domain, Req), - Domain = jid:nameprep(ExtDomain), - case mongoose_domain_sql:select_domain(Domain) of - {ok, Props} -> - {jiffy:encode(Props), Req, State}; - {error, not_found} -> - {stop, reply_error(404, <<"domain not found">>, Req), State} - end. - --spec handle_domain(Req, State) -> {boolean(), Req, State} - when Req :: cowboy_req:req(), State :: state(). -handle_domain(Req, State) -> - Method = cowboy_req:method(Req), - ExtDomain = cowboy_req:binding(domain, Req), - Domain = jid:nameprep(ExtDomain), - {ok, Body, Req2} = cowboy_req:read_body(Req), - MaybeParams = json_decode(Body), - case Method of - <<"PUT">> -> - insert_domain(Domain, MaybeParams, Req2, State); - <<"PATCH">> -> - patch_domain(Domain, MaybeParams, Req2, State) - end. - -%% Private helper functions: -insert_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) -> - case mongoose_domain_api:insert_domain(Domain, HostType) of - ok -> - {true, Req, State}; - {error, duplicate} -> - {false, reply_error(409, <<"duplicate">>, Req), State}; - {error, static} -> - {false, reply_error(403, <<"domain is static">>, Req), State}; - {error, {db_error, _}} -> - {false, reply_error(500, <<"database error">>, Req), State}; - {error, service_disabled} -> - {false, reply_error(403, <<"service disabled">>, Req), State}; - {error, unknown_host_type} -> - {false, reply_error(403, <<"unknown host type">>, Req), State} - end; -insert_domain(_Domain, {ok, #{}}, Req, State) -> - {false, reply_error(400, <<"'host_type' field is missing">>, Req), State}; -insert_domain(_Domain, {error, empty}, Req, State) -> - {false, reply_error(400, <<"body is empty">>, Req), State}; -insert_domain(_Domain, {error, _}, Req, State) -> - {false, reply_error(400, <<"failed to parse JSON">>, Req), State}. - -patch_domain(Domain, {ok, #{<<"enabled">> := true}}, Req, State) -> - Res = mongoose_domain_api:enable_domain(Domain), - handle_enabled_result(Res, Req, State); -patch_domain(Domain, {ok, #{<<"enabled">> := false}}, Req, State) -> - Res = mongoose_domain_api:disable_domain(Domain), - handle_enabled_result(Res, Req, State); -patch_domain(_Domain, {ok, #{}}, Req, State) -> - {false, reply_error(400, <<"'enabled' field is missing">>, Req), State}; -patch_domain(_Domain, {error, empty}, Req, State) -> - {false, reply_error(400, <<"body is empty">>, Req), State}; -patch_domain(_Domain, {error, _}, Req, State) -> - {false, reply_error(400, <<"failed to parse JSON">>, Req), State}. - -handle_enabled_result(Res, Req, State) -> - case Res of - ok -> - {true, Req, State}; - {error, not_found} -> - {false, reply_error(404, <<"domain not found">>, Req), State}; - {error, static} -> - {false, reply_error(403, <<"domain is static">>, Req), State}; - {error, service_disabled} -> - {false, reply_error(403, <<"service disabled">>, Req), State}; - {error, {db_error, _}} -> - {false, reply_error(500, <<"database error">>, Req), State} - end. - -delete_resource(Req, State) -> - ExtDomain = cowboy_req:binding(domain, Req), - Domain = jid:nameprep(ExtDomain), - {ok, Body, Req2} = cowboy_req:read_body(Req), - MaybeParams = json_decode(Body), - delete_domain(Domain, MaybeParams, Req2, State). - -delete_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) -> - case mongoose_domain_api:delete_domain(Domain, HostType) of - ok -> - {true, Req, State}; - {error, {db_error, _}} -> - {false, reply_error(500, <<"database error">>, Req), State}; - {error, static} -> - {false, reply_error(403, <<"domain is static">>, Req), State}; - {error, service_disabled} -> - {false, reply_error(403, <<"service disabled">>, Req), State}; - {error, wrong_host_type} -> - {false, reply_error(403, <<"wrong host type">>, Req), State}; - {error, unknown_host_type} -> - {false, reply_error(403, <<"unknown host type">>, Req), State} - end; -delete_domain(_Domain, {ok, #{}}, Req, State) -> - {false, reply_error(400, <<"'host_type' field is missing">>, Req), State}; -delete_domain(_Domain, {error, empty}, Req, State) -> - {false, reply_error(400, <<"body is empty">>, Req), State}; -delete_domain(_Domain, {error, _}, Req, State) -> - {false, reply_error(400, <<"failed to parse JSON">>, Req), State}. - -reply_error(Code, What, Req) -> - ?LOG_ERROR(#{what => rest_domain_failed, reason => What, - code => Code, req => Req}), - Body = jiffy:encode(#{what => What}), - cowboy_req:reply(Code, #{<<"content-type">> => <<"application/json">>}, Body, Req). - -json_decode(<<>>) -> - {error, empty}; -json_decode(Bin) -> - try - {ok, jiffy:decode(Bin, [return_maps])} - catch - Class:Reason -> - {error, {Class, Reason}} - end. diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index 6f75a168d56..d424324b23f 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -25,7 +25,7 @@ atom() => any()}. -type req() :: cowboy_req:req(). -type state() :: map(). --type error_type() :: bad_request | denied | not_found | internal. +-type error_type() :: bad_request | denied | not_found | duplicate | internal. %% mongoose_http_handler callbacks @@ -67,7 +67,8 @@ api_paths(Opts) -> {"/mucs/:domain", mongoose_admin_api_muc, Opts}, {"/mucs/:domain/:name/:arg", mongoose_admin_api_muc, Opts}, {"/inbox/:host_type/:days/bin", mongoose_admin_api_inbox, Opts}, - {"/inbox/:domain/:user/:days/bin", mongoose_admin_api_inbox, Opts} + {"/inbox/:domain/:user/:days/bin", mongoose_admin_api_inbox, Opts}, + {"/domains/:domain", mongoose_admin_api_domain, Opts} ]. %% Utilities for the handler modules @@ -166,10 +167,12 @@ error_response(ErrorType, Message, Req, State) -> error_code(bad_request) -> 400; error_code(denied) -> 403; error_code(not_found) -> 404; +error_code(duplicate) -> 409; error_code(internal) -> 500. -spec log_level(error_type()) -> logger:level(). log_level(bad_request) -> warning; log_level(denied) -> warning; log_level(not_found) -> warning; +log_level(duplicate) -> warning; log_level(internal) -> error. diff --git a/src/mongoose_admin_api/mongoose_admin_api_domain.erl b/src/mongoose_admin_api/mongoose_admin_api_domain.erl new file mode 100644 index 00000000000..907f3243f73 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_domain.erl @@ -0,0 +1,150 @@ +-module(mongoose_admin_api_domain). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2, + delete_resource/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_accepted(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, from_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"GET">>, <<"PATCH">>, <<"PUT">>, <<"DELETE">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "PUT" or "PATCH" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + F = case cowboy_req:method(Req) of + <<"PUT">> -> fun handle_put/2; + <<"PATCH">> -> fun handle_patch/2 + end, + try_handle_request(Req, State, F). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_get(Req, State) -> + Bindings = cowboy_req:bindings(Req), + Domain = get_domain(Bindings), + case mongoose_domain_sql:select_domain(Domain) of + {ok, Props} -> + {jiffy:encode(Props), Req, State}; + {error, not_found} -> + throw_error(not_found, <<"Domain not found">>) + end. + +handle_put(Req, State) -> + Bindings = cowboy_req:bindings(Req), + Domain = get_domain(Bindings), + Args = parse_body(Req), + HostType = get_host_type(Args), + case mongoose_domain_api:insert_domain(Domain, HostType) of + ok -> + {true, Req, State}; + {error, duplicate} -> + throw_error(duplicate, <<"Duplicate domain">>); + {error, static} -> + throw_error(denied, <<"Domain is static">>); + {error, {db_error, _}} -> + throw_error(internal, <<"Database error">>); + {error, service_disabled} -> + throw_error(denied, <<"Service disabled">>); + {error, unknown_host_type} -> + throw_error(denied, <<"Unknown host type">>) + end. + +handle_patch(Req, State) -> + Bindings = cowboy_req:bindings(Req), + Domain = get_domain(Bindings), + Args = parse_body(Req), + Result = case get_enabled(Args) of + true -> + mongoose_domain_api:enable_domain(Domain); + false -> + mongoose_domain_api:disable_domain(Domain) + end, + case Result of + ok -> + {true, Req, State}; + {error, not_found} -> + throw_error(not_found, <<"Domain not found">>); + {error, static} -> + throw_error(denied, <<"Domain is static">>); + {error, service_disabled} -> + throw_error(denied, <<"Service disabled">>); + {error, {db_error, _}} -> + throw_error(internal, <<"Database error">>) + end. + +handle_delete(Req, State) -> + Bindings = cowboy_req:bindings(Req), + Domain = get_domain(Bindings), + Args = parse_body(Req), + HostType = get_host_type(Args), + case mongoose_domain_api:delete_domain(Domain, HostType) of + ok -> + {true, Req, State}; + {error, {db_error, _}} -> + throw_error(internal, <<"Database error">>); + {error, static} -> + throw_error(denied, <<"Domain is static">>); + {error, service_disabled} -> + throw_error(denied, <<"Service disabled">>); + {error, wrong_host_type} -> + throw_error(denied, <<"Wrong host type">>); + {error, unknown_host_type} -> + throw_error(denied, <<"Unknown host type">>) + end. + +get_domain(#{domain := Domain}) -> + case jid:nameprep(Domain) of + error -> throw_error(bad_request, <<"Invalid domain name">>); + PrepDomain -> PrepDomain + end. + +get_host_type(#{host_type := HostType}) -> HostType; +get_host_type(#{}) -> throw_error(bad_request, <<"'host_type' field is missing">>). + +get_enabled(#{enabled := Enabled}) -> Enabled; +get_enabled(#{}) -> throw_error(bad_request, <<"'enabled' field is missing">>). diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index eee7434a57a..e1da3d77589 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -85,5 +85,4 @@ configurable_handler_modules() -> mongoose_client_api, mongoose_admin_api, mongoose_api, - mongoose_domain_handler, mongoose_graphql_cowboy_handler]. diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 2fcca06f7b7..b206be499be 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -127,7 +127,7 @@ get_api() -> filter_unknown_api(ApiList) -> AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_admin_api, - mongoose_domain_handler, mod_bosh, mod_websockets], + mod_bosh, mod_websockets], [Api || Api <- ApiList, lists:member(Api, AllowedToReport)]. get_transport_mechanisms() -> From c8be0bea5dd9f330f8c0b590f376635065e1ee8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 09:04:45 +0200 Subject: [PATCH 057/117] Remove tests for the deleted domain handler --- test/config_parser_SUITE.erl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index d65ca2832d8..5a9dfaa90ec 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -99,7 +99,6 @@ groups() -> listen_http_handlers_client_api, listen_http_handlers_admin_api, listen_http_handlers_api, - listen_http_handlers_domain, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, auth_password, @@ -613,10 +612,6 @@ listen_http_handlers_api(_Config) -> T(#{<<"handlers">> => [<<"mongoose_api_metrics">>]})), ?err(T(#{<<"handlers">> => [not_a_module]})). -listen_http_handlers_domain(_Config) -> - {P, T} = test_listen_http_handler(mongoose_domain_handler), - test_listen_http_handler_creds(P, T). - listen_http_handlers_graphql(_Config) -> T = fun graphql_handler_raw/1, {P, _} = test_listen_http_handler(mongoose_graphql_cowboy_handler, T), From cecd6e42a20670059078a886da3917fbe219e182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 09:05:21 +0200 Subject: [PATCH 058/117] Remove the domain handler from the TOML config --- rel/files/mongooseim.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index 79c8c382d03..7e25bc74b2e 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -60,10 +60,6 @@ host = "localhost" path = "/api" - [[listen.http.handlers.mongoose_domain_handler]] - host = "localhost" - path = "/api" - [[listen.http]] {{#http_api_client_endpoint}} {{{http_api_client_endpoint}}} From 33b5d9c14bd9800eb3f60da60a9905f2cc638094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 09:05:49 +0200 Subject: [PATCH 059/117] Adjust big tests for the domain REST API --- big_tests/tests/domain_rest_helper.erl | 39 ------- big_tests/tests/service_domain_db_SUITE.erl | 119 +++++++------------- 2 files changed, 42 insertions(+), 116 deletions(-) diff --git a/big_tests/tests/domain_rest_helper.erl b/big_tests/tests/domain_rest_helper.erl index c85268ec11a..b41d77e8c1c 100644 --- a/big_tests/tests/domain_rest_helper.erl +++ b/big_tests/tests/domain_rest_helper.erl @@ -14,14 +14,7 @@ delete_custom/4, patch_custom/4]). -%% Handler --export([start_listener/1, - stop_listener/1]). - -import(distributed_helper, [mim/0, mim2/0, require_rpc_nodes/1, rpc/4]). --import(config_parser_helper, [default_config/1, config/2]). - --define(TEST_PORT, 8866). set_invalid_creds(Config) -> [{auth_creds, invalid}|Config]. @@ -49,7 +42,6 @@ make_creds(Config) -> rest_patch_enabled(Config, Domain, Enabled) -> Params = #{enabled => Enabled}, rest_helper:make_request(#{ role => admin, method => <<"PATCH">>, - port => ?TEST_PORT, path => domain_path(Domain), creds => make_creds(Config), body => Params }). @@ -57,14 +49,12 @@ rest_patch_enabled(Config, Domain, Enabled) -> rest_put_domain(Config, Domain, Type) -> Params = #{host_type => Type}, rest_helper:make_request(#{ role => admin, method => <<"PUT">>, - port => ?TEST_PORT, path => domain_path(Domain), creds => make_creds(Config), body => Params }). putt_domain_with_custom_body(Config, Body) -> rest_helper:make_request(#{ role => admin, method => <<"PUT">>, - port => ?TEST_PORT, path => <<"/domains/example.db">>, creds => make_creds(Config), body => Body }). @@ -72,7 +62,6 @@ putt_domain_with_custom_body(Config, Body) -> rest_select_domain(Config, Domain) -> Params = #{}, rest_helper:make_request(#{ role => admin, method => <<"GET">>, - port => ?TEST_PORT, path => domain_path(Domain), creds => make_creds(Config), body => Params }). @@ -80,44 +69,16 @@ rest_select_domain(Config, Domain) -> rest_delete_domain(Config, Domain, HostType) -> Params = #{<<"host_type">> => HostType}, rest_helper:make_request(#{ role => admin, method => <<"DELETE">>, - port => ?TEST_PORT, path => domain_path(Domain), creds => make_creds(Config), body => Params }). delete_custom(Config, Role, Path, Body) -> rest_helper:make_request(#{ role => Role, method => <<"DELETE">>, - port => ?TEST_PORT, path => Path, creds => make_creds(Config), body => Body }). patch_custom(Config, Role, Path, Body) -> rest_helper:make_request(#{ role => Role, method => <<"PATCH">>, - port => ?TEST_PORT, path => Path, creds => make_creds(Config), body => Body }). - -%% REST handler setup -start_listener(Params) -> - rpc(mim(), mongoose_listener, start_listener, [listener_opts(Params)]). - -stop_listener(Params) -> - rpc(mim(), mongoose_listener, stop_listener, [listener_opts(Params)]). - -listener_opts(Params) -> - config([listen, http], - #{port => ?TEST_PORT, - ip_tuple => {127, 0, 0, 1}, - ip_address => "127.0.0.1", - module => ejabberd_cowboy, - handlers => [domain_handler(Params)], - transport => config([listen, http, transport], #{num_acceptors => 10})}). - -domain_handler(Params) -> - maps:merge(#{host => "localhost", path => "/api", module => mongoose_domain_handler}, - handler_opts(Params)). - -handler_opts(#{skip_auth := true}) -> - #{}; -handler_opts(_Params) -> - #{password => <<"secret">>, username => <<"admin">>}. diff --git a/big_tests/tests/service_domain_db_SUITE.erl b/big_tests/tests/service_domain_db_SUITE.erl index 54f667e150a..126f0f7ed80 100644 --- a/big_tests/tests/service_domain_db_SUITE.erl +++ b/big_tests/tests/service_domain_db_SUITE.erl @@ -20,17 +20,9 @@ delete_custom/4, patch_custom/4]). --import(domain_rest_helper, - [start_listener/1, - stop_listener/1]). - -import(domain_helper, [domain/0]). -import(config_parser_helper, [config/2]). --define(INV_PWD, <<"basic auth provided, invalid password">>). --define(NO_PWD, <<"basic auth is required">>). --define(UNWANTED_PWD, <<"basic auth provided, but not configured">>). - suite() -> require_rpc_nodes([mim, mim2, mim3]). @@ -209,11 +201,8 @@ init_per_group(db, Config) -> false -> {skip, require_rdbms} end; init_per_group(rest_with_auth, Config) -> - start_listener(#{}), + rest_helper:change_admin_creds({<<"admin">>, <<"secret">>}), [{auth_creds, valid}|Config]; -init_per_group(rest_without_auth, Config) -> - start_listener(#{skip_auth => true}), - Config; init_per_group(GroupName, Config) -> Config1 = save_service_setup_option(GroupName, Config), case ?config(service_setup, Config) of @@ -223,9 +212,7 @@ init_per_group(GroupName, Config) -> Config1. end_per_group(rest_with_auth, _Config) -> - stop_listener(#{}); -end_per_group(rest_without_auth, _Config) -> - stop_listener(#{skip_auth => true}); + rest_helper:change_admin_creds(any); end_per_group(_GroupName, Config) -> case ?config(service_setup, Config) of per_group -> teardown_service(); @@ -841,8 +828,7 @@ rest_can_delete_domain(Config) -> rest_cannot_delete_domain_without_correct_type(Config) -> rest_put_domain(Config, <<"example.db">>, <<"type1">>), - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"wrong host type">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Wrong host type">>} = rest_delete_domain(Config, <<"example.db">>, <<"type2">>), {ok, _} = select_domain(mim(), <<"example.db">>). @@ -851,90 +837,88 @@ rest_delete_missing_domain(Config) -> rest_delete_domain(Config, <<"example.db">>, <<"type1">>). rest_cannot_enable_missing_domain(Config) -> - {{<<"404">>, <<"Not Found">>}, - {[{<<"what">>, <<"domain not found">>}]}} = + {{<<"404">>, <<"Not Found">>}, <<"Domain not found">>} = rest_patch_enabled(Config, <<"example.db">>, true). rest_cannot_insert_domain_twice_with_another_host_type(Config) -> rest_put_domain(Config, <<"example.db">>, <<"type1">>), - {{<<"409">>, <<"Conflict">>}, {[{<<"what">>, <<"duplicate">>}]}} = + {{<<"409">>, <<"Conflict">>}, <<"Duplicate domain">>} = rest_put_domain(Config, <<"example.db">>, <<"type2">>). rest_cannot_insert_domain_with_unknown_host_type(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, <<"unknown host type">>}]}} = + {{<<"403">>,<<"Forbidden">>}, <<"Unknown host type">>} = rest_put_domain(Config, <<"example.db">>, <<"type6">>). rest_cannot_delete_domain_with_unknown_host_type(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, <<"unknown host type">>}]}} = + {{<<"403">>,<<"Forbidden">>}, <<"Unknown host type">>} = rest_delete_domain(Config, <<"example.db">>, <<"type6">>). %% auth provided, but not configured: rest_cannot_insert_domain_if_auth_provided_but_not_configured(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?UNWANTED_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_put_domain(set_valid_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_delete_domain_if_auth_provided_but_not_configured(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?UNWANTED_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_delete_domain(set_valid_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_enable_domain_if_auth_provided_but_not_configured(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?UNWANTED_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_valid_creds(Config), <<"example.db">>, false). rest_cannot_disable_domain_if_auth_provided_but_not_configured(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?UNWANTED_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_valid_creds(Config), <<"example.db">>, false). rest_cannot_select_domain_if_auth_provided_but_not_configured(Config) -> - {{<<"403">>, <<"Forbidden">>}, {[{<<"what">>, ?UNWANTED_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_select_domain(set_valid_creds(Config), <<"example.db">>). %% with wrong pass: rest_cannot_insert_domain_with_wrong_pass(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?INV_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_put_domain(set_invalid_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_delete_domain_with_wrong_pass(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?INV_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_delete_domain(set_invalid_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_enable_domain_with_wrong_pass(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?INV_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_invalid_creds(Config), <<"example.db">>, true). rest_cannot_disable_domain_with_wrong_pass(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?INV_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_invalid_creds(Config), <<"example.db">>, false). rest_cannot_select_domain_with_wrong_pass(Config) -> - {{<<"403">>, <<"Forbidden">>}, {[{<<"what">>, ?INV_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_select_domain(set_invalid_creds(Config), <<"example.db">>). %% without auth: rest_cannot_insert_domain_without_auth(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?NO_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_put_domain(set_no_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_delete_domain_without_auth(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?NO_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_delete_domain(set_no_creds(Config), <<"example.db">>, <<"type1">>). rest_cannot_enable_domain_without_auth(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?NO_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_no_creds(Config), <<"example.db">>, true). rest_cannot_disable_domain_without_auth(Config) -> - {{<<"403">>,<<"Forbidden">>}, {[{<<"what">>, ?NO_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_patch_enabled(set_no_creds(Config), <<"example.db">>, false). rest_cannot_select_domain_without_auth(Config) -> - {{<<"403">>, <<"Forbidden">>}, {[{<<"what">>, ?NO_PWD}]}} = + {{<<"401">>, <<"Unauthorized">>}, _} = rest_select_domain(set_no_creds(Config), <<"example.db">>). rest_cannot_disable_missing_domain(Config) -> - {{<<"404">>, <<"Not Found">>}, - {[{<<"what">>, <<"domain not found">>}]}} = + {{<<"404">>, <<"Not Found">>}, <<"Domain not found">>} = rest_patch_enabled(Config, <<"example.db">>, false). rest_can_enable_domain(Config) -> @@ -951,104 +935,85 @@ rest_can_select_domain(Config) -> rest_select_domain(Config, <<"example.db">>). rest_cannot_select_domain_if_domain_not_found(Config) -> - {{<<"404">>, <<"Not Found">>}, - {[{<<"what">>, <<"domain not found">>}]}} = + {{<<"404">>, <<"Not Found">>}, <<"Domain not found">>} = rest_select_domain(Config, <<"example.db">>). rest_cannot_put_domain_without_host_type(Config) -> - {{<<"400">>, <<"Bad Request">>}, - {[{<<"what">>, <<"'host_type' field is missing">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"'host_type' field is missing">>} = putt_domain_with_custom_body(Config, #{}). rest_cannot_put_domain_without_body(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"body is empty">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"Invalid request body">>} = putt_domain_with_custom_body(Config, <<>>). rest_cannot_put_domain_with_invalid_json(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"failed to parse JSON">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"Invalid request body">>} = putt_domain_with_custom_body(Config, <<"{kek">>). rest_cannot_put_domain_when_it_is_static(Config) -> - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"domain is static">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Domain is static">>} = rest_put_domain(Config, <<"example.cfg">>, <<"type1">>). rest_cannot_delete_domain_without_host_type(Config) -> - {{<<"400">>, <<"Bad Request">>}, - {[{<<"what">>, <<"'host_type' field is missing">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"'host_type' field is missing">>} = delete_custom(Config, admin, <<"/domains/example.db">>, #{}). rest_cannot_delete_domain_without_body(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"body is empty">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"Invalid request body">>} = delete_custom(Config, admin, <<"/domains/example.db">>, <<>>). rest_cannot_delete_domain_with_invalid_json(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"failed to parse JSON">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"Invalid request body">>} = delete_custom(Config, admin, <<"/domains/example.db">>, <<"{kek">>). rest_cannot_delete_domain_when_it_is_static(Config) -> - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"domain is static">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Domain is static">>} = rest_delete_domain(Config, <<"example.cfg">>, <<"type1">>). rest_cannot_patch_domain_without_enabled_field(Config) -> - {{<<"400">>, <<"Bad Request">>}, - {[{<<"what">>, <<"'enabled' field is missing">>}]}} = + {{<<"400">>, <<"Bad Request">>}, <<"'enabled' field is missing">>} = patch_custom(Config, admin, <<"/domains/example.db">>, #{}). rest_cannot_patch_domain_without_body(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"body is empty">>}]}} = + {{<<"400">>,<<"Bad Request">>}, <<"Invalid request body">>} = patch_custom(Config, admin, <<"/domains/example.db">>, <<>>). rest_cannot_patch_domain_with_invalid_json(Config) -> - {{<<"400">>,<<"Bad Request">>}, - {[{<<"what">>,<<"failed to parse JSON">>}]}} = + {{<<"400">>,<<"Bad Request">>}, <<"Invalid request body">>} = patch_custom(Config, admin, <<"/domains/example.db">>, <<"{kek">>). %% SQL query is mocked to fail rest_insert_domain_fails_if_db_fails(Config) -> - {{<<"500">>, <<"Internal Server Error">>}, - {[{<<"what">>, <<"database error">>}]}} = + {{<<"500">>, <<"Internal Server Error">>}, <<"Database error">>} = rest_put_domain(Config, <<"example.db">>, <<"type1">>). rest_insert_domain_fails_if_service_disabled(Config) -> service_disabled(mim()), - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"service disabled">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Service disabled">>} = rest_put_domain(Config, <<"example.db">>, <<"type1">>). %% SQL query is mocked to fail rest_delete_domain_fails_if_db_fails(Config) -> - {{<<"500">>, <<"Internal Server Error">>}, - {[{<<"what">>, <<"database error">>}]}} = + {{<<"500">>, <<"Internal Server Error">>}, <<"Database error">>} = rest_delete_domain(Config, <<"example.db">>, <<"type1">>). rest_delete_domain_fails_if_service_disabled(Config) -> service_disabled(mim()), - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"service disabled">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Service disabled">>} = rest_delete_domain(Config, <<"example.db">>, <<"type1">>). %% SQL query is mocked to fail rest_enable_domain_fails_if_db_fails(Config) -> - {{<<"500">>, <<"Internal Server Error">>}, - {[{<<"what">>, <<"database error">>}]}} = + {{<<"500">>, <<"Internal Server Error">>}, <<"Database error">>} = rest_patch_enabled(Config, <<"example.db">>, true). rest_enable_domain_fails_if_service_disabled(Config) -> service_disabled(mim()), - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"service disabled">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Service disabled">>} = rest_patch_enabled(Config, <<"example.db">>, true). rest_cannot_enable_domain_when_it_is_static(Config) -> - {{<<"403">>, <<"Forbidden">>}, - {[{<<"what">>, <<"domain is static">>}]}} = + {{<<"403">>, <<"Forbidden">>}, <<"Domain is static">>} = rest_patch_enabled(Config, <<"example.cfg">>, true). rest_delete_domain_cleans_data_from_mam(Config) -> From 9082e6e30ba500766800836ae0f2e83e40f0093f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Thu, 22 Sep 2022 11:46:11 +0200 Subject: [PATCH 060/117] Improve test coverage Cover the unlikely case of an invalid domain name. --- big_tests/tests/service_domain_db_SUITE.erl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/big_tests/tests/service_domain_db_SUITE.erl b/big_tests/tests/service_domain_db_SUITE.erl index 126f0f7ed80..dad9c03f78e 100644 --- a/big_tests/tests/service_domain_db_SUITE.erl +++ b/big_tests/tests/service_domain_db_SUITE.erl @@ -148,6 +148,7 @@ rest_cases() -> rest_cannot_put_domain_without_host_type, rest_cannot_put_domain_without_body, rest_cannot_put_domain_with_invalid_json, + rest_cannot_put_domain_with_invalid_name, rest_cannot_put_domain_when_it_is_static, rest_cannot_delete_domain_without_host_type, rest_cannot_delete_domain_without_body, @@ -950,6 +951,10 @@ rest_cannot_put_domain_with_invalid_json(Config) -> {{<<"400">>, <<"Bad Request">>}, <<"Invalid request body">>} = putt_domain_with_custom_body(Config, <<"{kek">>). +rest_cannot_put_domain_with_invalid_name(Config) -> + {{<<"400">>, <<"Bad Request">>}, <<"Invalid domain name">>} = + rest_put_domain(Config, <<"%f3">>, <<"type1">>). % nameprep fails for ASCII code 243 + rest_cannot_put_domain_when_it_is_static(Config) -> {{<<"403">>, <<"Forbidden">>}, <<"Domain is static">>} = rest_put_domain(Config, <<"example.cfg">>, <<"type1">>). From 085b29b46da4232f94bf2f315149af3fb84d1346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:12:18 +0200 Subject: [PATCH 061/117] Add metrics API handler with the logic from mongoose_api_metrics Although the functionality is not complete (e.g. some requests support only atoms as metic names), this is not changed here. The goal is to only replicate the existing functionality in the new unified handlers. --- .../mongoose_admin_api_metrics.erl | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/mongoose_admin_api/mongoose_admin_api_metrics.erl diff --git a/src/mongoose_admin_api/mongoose_admin_api_metrics.erl b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl new file mode 100644 index 00000000000..c227885fd14 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl @@ -0,0 +1,138 @@ +-module(mongoose_admin_api_metrics). +-behaviour(cowboy_rest). + +-export([init/2, + is_authorized/2, + content_types_provided/2, + allowed_methods/2, + to_json/2]). + +-ignore_xref([to_json/2, from_json/2]). + +-import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + +-include("mongoose.hrl"). + +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, Opts) -> + mongoose_admin_api:init(Req, Opts). + +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. +is_authorized(Req, State) -> + mongoose_admin_api:is_authorized(Req, State). + +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. +content_types_provided(Req, State) -> + {[ + {{<<"application">>, <<"json">>, '*'}, to_json} + ], Req, State}. + +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. +allowed_methods(Req, State) -> + {[<<"OPTIONS">>, <<"HEAD">>, <<"GET">>], Req, State}. + +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% Internal functions + +handle_get(Req, State = #{suffix := all}) -> + Bindings = cowboy_req:bindings(Req), + case get_metric_name(Bindings) of + {metric, Metric} -> + case mongoose_metrics:get_aggregated_values(Metric) of + [] -> + throw_error(not_found, <<"Metric not found">>); + Value -> + {jiffy:encode(#{metric => prepare_value(Value)}), Req, State} + end; + all_metrics -> + Values = get_sum_metrics(), + {jiffy:encode(#{metrics => Values}), Req, State} + end; +handle_get(Req, State = #{suffix := global}) -> + Bindings = cowboy_req:bindings(Req), + handle_get_values(Req, State, Bindings, global); +handle_get(Req, State) -> + Bindings = cowboy_req:bindings(Req), + case Bindings of + #{host_type := HostType} -> + handle_get_values(Req, State, Bindings, HostType); + #{} -> + {HostTypes, Metrics} = get_available_host_type_metrics(), + Global = get_available_global_metrics(), + Reply = #{host_types => HostTypes, metrics => Metrics, global => Global}, + {jiffy:encode(Reply), Req, State} + end. + +handle_get_values(Req, State, Bindings, HostType) -> + case get_metric_name(Bindings) of + {metric, Metric} -> + case mongoose_metrics:get_metric_value(HostType, Metric) of + {ok, Value} -> + {jiffy:encode(#{metric => prepare_value(Value)}), Req, State}; + _Other -> + throw_error(not_found, <<"Metric not found">>) + end; + all_metrics -> + case mongoose_metrics:get_metric_values(HostType) of + [] -> + throw_error(not_found, <<"No metrics found">>); + Metrics -> + Values = prepare_metrics(Metrics), + {jiffy:encode(#{metrics => Values}), Req, State} + end + end. + +-spec get_sum_metrics() -> map(). +get_sum_metrics() -> + {_HostTypes, Metrics} = get_available_host_type_metrics(), + maps:from_list([{Metric, get_sum_metric(Metric)} || Metric <- Metrics]). + +-spec get_sum_metric(atom()) -> map(). +get_sum_metric(Metric) -> + maps:from_list(mongoose_metrics:get_aggregated_values(Metric)). + +-spec get_available_metrics(HostType :: mongooseim:host_type()) -> [any()]. +get_available_metrics(HostType) -> + mongoose_metrics:get_host_type_metric_names(HostType). + +-spec get_available_host_type_metrics() -> {[any(), ...], [any()]}. +get_available_host_type_metrics() -> + HostTypes = get_available_host_types(), + Metrics = [Metric || [Metric] <- get_available_metrics(hd(HostTypes))], + {HostTypes, Metrics}. + +get_available_global_metrics() -> + [Metric || [Metric] <- mongoose_metrics:get_global_metric_names()]. + +-spec get_available_host_types() -> [mongooseim:host_type()]. +get_available_host_types() -> + ?ALL_HOST_TYPES. + +prepare_metrics(Metrics) -> + maps:from_list([{prepare_name(NameParts), prepare_value(Value)} + || {[_HostType | NameParts], Value} <- Metrics]). + +prepare_name(NameParts) -> + ToStrings = [atom_to_list(NamePart) || NamePart <- NameParts], + list_to_binary(string:join(ToStrings, ".")). + +prepare_value(KVs) -> + maps:from_list([{prepare_key(K), V} || {K, V} <- KVs]). + +prepare_key(K) when is_integer(K) -> integer_to_binary(K); +prepare_key(K) when is_atom(K) -> atom_to_binary(K). + +get_metric_name(#{metric := MetricBin}) -> + try {metric, binary_to_existing_atom(MetricBin)} + catch _:_ -> throw_error(not_found, <<"Metric not found">>) + end; +get_metric_name(#{}) -> + all_metrics. From e36eaac0d6cd37382df6f5b6f442df30bb138ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:14:38 +0200 Subject: [PATCH 062/117] Add the metric handlers to mongoose_admin_api --- src/mongoose_admin_api/mongoose_admin_api.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index d424324b23f..53df70d857b 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -68,7 +68,11 @@ api_paths(Opts) -> {"/mucs/:domain/:name/:arg", mongoose_admin_api_muc, Opts}, {"/inbox/:host_type/:days/bin", mongoose_admin_api_inbox, Opts}, {"/inbox/:domain/:user/:days/bin", mongoose_admin_api_inbox, Opts}, - {"/domains/:domain", mongoose_admin_api_domain, Opts} + {"/domains/:domain", mongoose_admin_api_domain, Opts}, + {"/metrics/", mongoose_admin_api_metrics, Opts}, + {"/metrics/all/[:metric]", mongoose_admin_api_metrics, Opts#{suffix => all}}, + {"/metrics/global/[:metric]", mongoose_admin_api_metrics, Opts#{suffix => global}}, + {"/metrics/host_type/:host_type/[:metric]", mongoose_admin_api_metrics, Opts} ]. %% Utilities for the handler modules From 84ea45ec06718c1a649eac7dea3c24b3158bec25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:19:00 +0200 Subject: [PATCH 063/117] Disable the old mongoose_api in the config file --- rel/fed1.vars-toml.config | 3 --- rel/files/mongooseim.toml | 11 ----------- rel/mim2.vars-toml.config | 3 --- rel/mim3.vars-toml.config | 3 --- rel/reg1.vars-toml.config | 3 --- rel/vars-toml.config | 2 -- test/common/config_parser_helper.erl | 9 --------- test/config_parser_SUITE_data/mongooseim-pgsql.toml | 10 ---------- 8 files changed, 44 deletions(-) diff --git a/rel/fed1.vars-toml.config b/rel/fed1.vars-toml.config index 78ece4941a5..25b9d6cb45f 100644 --- a/rel/fed1.vars-toml.config +++ b/rel/fed1.vars-toml.config @@ -7,7 +7,6 @@ {http_port, 5282}. {https_port, 5287}. {http_api_endpoint_port, 5294}. -{http_api_old_endpoint_port, 5293}. {http_api_client_endpoint_port, 8095}. {http_graphql_api_admin_endpoint_port, 5556}. {http_graphql_api_domain_admin_endpoint_port, 5546}. @@ -48,8 +47,6 @@ tls.mode = \"starttls\" tls.ciphers = \"ECDHE-RSA-AES256-GCM-SHA384\""}. -{http_api_old_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_old_endpoint_port }}"}. {http_api_endpoint, "ip_address = \"127.0.0.1\" port = {{ http_api_endpoint_port }}"}. {http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index 7e25bc74b2e..b9f12446eeb 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -113,17 +113,6 @@ path = "/api/graphql" schema_endpoint = "user" -[[listen.http]] - {{#http_api_old_endpoint}} - {{{http_api_old_endpoint}}} - {{/http_api_old_endpoint}} - transport.num_acceptors = 10 - transport.max_connections = 1024 - - [[listen.http.handlers.mongoose_api]] - host = "localhost" - path = "/api" - [[listen.c2s]] port = {{{c2s_port}}} {{#zlib}} diff --git a/rel/mim2.vars-toml.config b/rel/mim2.vars-toml.config index 89c144e4500..c155e82df7b 100644 --- a/rel/mim2.vars-toml.config +++ b/rel/mim2.vars-toml.config @@ -7,7 +7,6 @@ {incoming_s2s_port, 5279}. {http_port, 5281}. {https_port, 5286}. -{http_api_old_endpoint_port, 5289}. {http_api_endpoint_port, 8090}. {http_api_client_endpoint_port, 8091}. {service_port, 8899}. @@ -30,8 +29,6 @@ port = {{http_graphql_api_domain_admin_endpoint_port}}"}. {http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_old_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_old_endpoint_port }}"}. {http_api_endpoint, "ip_address = \"127.0.0.1\" port = {{ http_api_endpoint_port }}"}. {http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. diff --git a/rel/mim3.vars-toml.config b/rel/mim3.vars-toml.config index 0687b7deac9..4b33dbca101 100644 --- a/rel/mim3.vars-toml.config +++ b/rel/mim3.vars-toml.config @@ -8,7 +8,6 @@ {incoming_s2s_port, 5291}. {http_port, 5283}. {https_port, 5290}. -{http_api_old_endpoint_port, 5292}. {http_api_endpoint_port, 8092}. {http_api_client_endpoint_port, 8193}. {http_graphql_api_admin_endpoint_port, 5553}. @@ -48,8 +47,6 @@ port = {{http_graphql_api_domain_admin_endpoint_port}}"}. {http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_old_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_old_endpoint_port }}"}. {http_api_endpoint, "ip_address = \"127.0.0.1\" port = {{ http_api_endpoint_port }}"}. {http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. diff --git a/rel/reg1.vars-toml.config b/rel/reg1.vars-toml.config index ac14930954f..ee541b2a75c 100644 --- a/rel/reg1.vars-toml.config +++ b/rel/reg1.vars-toml.config @@ -8,7 +8,6 @@ {https_port, 5277}. {service_port, 9990}. {http_api_endpoint_port, 8074}. -{http_api_old_endpoint_port, 5273}. {http_api_client_endpoint_port, 8075}. {http_qraphql_api_admin_endpoint_port, 5554}. {http_graphql_api_domain_admin_endpoint_port, 5544}. @@ -48,8 +47,6 @@ port = {{http_graphql_api_domain_admin_endpoint_port}}"}. {http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_old_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_old_endpoint_port }}"}. {http_api_endpoint, "ip_address = \"127.0.0.1\" port = {{ http_api_endpoint_port }}"}. {http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. diff --git a/rel/vars-toml.config b/rel/vars-toml.config index 8e8d91d8d5c..22a74cc83f4 100644 --- a/rel/vars-toml.config +++ b/rel/vars-toml.config @@ -40,8 +40,6 @@ tls.certfile = \"priv/ssl/fake_cert.pem\" tls.keyfile = \"priv/ssl/fake_key.pem\" tls.password = \"\""}. -{http_api_old_endpoint, "ip_address = \"127.0.0.1\" - port = 5288"}. {http_graphql_api_admin_endpoint, "ip_address = \"127.0.0.1\" port = {{http_graphql_api_admin_endpoint_port}}"}. {http_graphql_api_domain_admin_endpoint, "ip_address = \"0.0.0.0\" diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index ef9c7ad92a8..81dde621358 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -195,15 +195,6 @@ options("mongooseim-pgsql") -> transport => #{num_acceptors => 10, max_connections => 1024}, tls => #{certfile => "priv/cert.pem", keyfile => "priv/dc1.pem", password => ""} }), - config([listen, http], - #{ip_address => "127.0.0.1", - ip_tuple => {127, 0, 0, 1}, - port => 5288, - transport => #{num_acceptors => 10, max_connections => 1024}, - handlers => - [config([listen, http, handlers, mongoose_api], - #{host => "localhost", path => "/api"})] - }), config([listen, s2s], #{port => 5269, shaper => s2s_shaper, diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index 3f4f4e8e475..98c2a87a6e4 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -78,16 +78,6 @@ host = "_" path = "/api" -[[listen.http]] - port = 5288 - ip_address = "127.0.0.1" - transport.num_acceptors = 10 - transport.max_connections = 1024 - - [[listen.http.handlers.mongoose_api]] - host = "localhost" - path = "/api" - [[listen.c2s]] port = 5222 zlib = 10000 From 856b66b0739f021563617e15497281d1a112a076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:19:30 +0200 Subject: [PATCH 064/117] Update metrics tests for the unified API --- big_tests/test.config | 2 - big_tests/tests/metrics_api_SUITE.erl | 95 +++++++++++---------------- 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/big_tests/test.config b/big_tests/test.config index cf890598aa6..8a62490d0e4 100644 --- a/big_tests/test.config +++ b/big_tests/test.config @@ -30,7 +30,6 @@ {muc_light_service_pattern, <<"muclight.@HOST@">>}, {s2s_port, 5269}, {incoming_s2s_port, 5269}, - {metrics_rest_port, 5288}, {c2s_port, 5222}, {c2s_tls_port, 5223}, {cowboy_port, 5280}, @@ -47,7 +46,6 @@ {vars, "mim2"}, {cluster, mim}, {c2s_tls_port, 5233}, - {metrics_rest_port, 5289}, {gd_endpoint_port, 6666}, {service_port, 8899}]}, {mim3, [{node, mongooseim3@localhost}, diff --git a/big_tests/tests/metrics_api_SUITE.erl b/big_tests/tests/metrics_api_SUITE.erl index 27681d968cd..7f2b0da9c24 100644 --- a/big_tests/tests/metrics_api_SUITE.erl +++ b/big_tests/tests/metrics_api_SUITE.erl @@ -16,11 +16,8 @@ -module(metrics_api_SUITE). -compile([export_all, nowarn_export_all]). --include_lib("common_test/include/ct.hrl"). - --import(distributed_helper, [mim/0, rpc/4]). --import(rest_helper, [assert_status/2, simple_request/2, simple_request/3, simple_request/4]). --define(PORT, (ct:get_config({hosts, mim, metrics_rest_port}))). +-import(distributed_helper, [mim/0, mim2/0, rpc/4]). +-import(rest_helper, [assert_status/2, make_request/1]). -include_lib("eunit/include/eunit.hrl"). @@ -245,10 +242,9 @@ cluster_size(Config) -> %%-------------------------------------------------------------------- metrics_only_global(_Config) -> - Port = ct:get_config({hosts, mim2, metrics_rest_port}), % 0. GET is the only implemented allowed method % (both OPTIONS and HEAD are for free then) - Res = simple_request(<<"OPTIONS">>, "/metrics/", Port), + Res = request(<<"OPTIONS">>, "/metrics/", mim2()), {_S, H, _B} = Res, assert_status(200, Res), V = proplists:get_value(<<"allow">>, H), @@ -256,7 +252,7 @@ metrics_only_global(_Config) -> ?assertEqual([<<"GET">>,<<"HEAD">>,<<"OPTIONS">>], lists:sort(Opts)), % List of host types and metrics - Res2 = simple_request(<<"GET">>, "/metrics/", Port), + Res2 = request(<<"GET">>, "/metrics/", mim2()), {_S2, _H2, B2} = Res2, assert_status(200, Res2), #{<<"host_types">> := [_ExampleHostType | _], @@ -264,16 +260,14 @@ metrics_only_global(_Config) -> <<"global">> := [ExampleGlobal | _]} = B2, % All global metrics - Res3 = simple_request(<<"GET">>, "/metrics/global", Port), + Res3 = request(<<"GET">>, "/metrics/global", mim2()), {_S3, _H3, B3} = Res3, assert_status(200, Res3), #{<<"metrics">> := _ML} = B3, ?assertEqual(1, maps:size(B3)), % An example global metric - Res4 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/global/", ExampleGlobal]), - Port), + Res4 = request(<<"GET">>, ["/metrics/global/", ExampleGlobal], mim2()), {_S4, _H4, B4} = Res4, #{<<"metric">> := _} = B4, ?assertEqual(1, maps:size(B4)). @@ -281,7 +275,7 @@ metrics_only_global(_Config) -> metrics_msg_flow(_Config) -> % 0. GET is the only implemented allowed method % (both OPTIONS and HEAD are for free then) - Res = simple_request(<<"OPTIONS">>, "/metrics/", ?PORT), + Res = request(<<"OPTIONS">>, "/metrics/"), {_S, H, _B} = Res, assert_status(200, Res), V = proplists:get_value(<<"allow">>, H), @@ -289,7 +283,7 @@ metrics_msg_flow(_Config) -> ?assertEqual([<<"GET">>,<<"HEAD">>,<<"OPTIONS">>], lists:sort(Opts)), % List of host types and metrics - Res2 = simple_request(<<"GET">>, "/metrics/", ?PORT), + Res2 = request(<<"GET">>, "/metrics/"), {_S2, _H2, B2} = Res2, assert_status(200, Res2), #{<<"host_types">> := [ExampleHostType | _], @@ -297,63 +291,51 @@ metrics_msg_flow(_Config) -> <<"global">> := [ExampleGlobal | _]} = B2, % Sum of all metrics - Res3 = simple_request(<<"GET">>, "/metrics/all", ?PORT), + Res3 = request(<<"GET">>, "/metrics/all"), {_S3, _H3, B3} = Res3, assert_status(200, Res3), #{<<"metrics">> := _ML} = B3, ?assertEqual(1, maps:size(B3)), % Sum for a given metric - Res4 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/all/", ExampleMetric]), - ?PORT), + Res4 = request(<<"GET">>, ["/metrics/all/", ExampleMetric]), {_S4, _H4, B4} = Res4, #{<<"metric">> := #{<<"one">> := _, <<"count">> := _} = IM} = B4, ?assertEqual(2, maps:size(IM)), ?assertEqual(1, maps:size(B4)), % Negative case for a non-existent given metric - Res5 = simple_request(<<"GET">>, "/metrics/all/nonExistentMetric", ?PORT), + Res5 = request(<<"GET">>, "/metrics/all/nonExistentMetric"), assert_status(404, Res5), % All metrics for an example host type - Res6 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/host_type/", ExampleHostType]), - ?PORT), + Res6 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType]), {_S6, _H6, B6} = Res6, #{<<"metrics">> := _} = B6, ?assertEqual(1, maps:size(B6)), % Negative case for a non-existent host type - Res7 = simple_request(<<"GET">>, "/metrics/host_type/nonExistentHostType", ?PORT), + Res7 = request(<<"GET">>, "/metrics/host_type/nonExistentHostType"), assert_status(404, Res7), % An example metric for an example host type - Res8 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/host_type/", ExampleHostType, - "/", ExampleMetric]), - ?PORT), + Res8 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType, "/", ExampleMetric]), {_S8, _H8, B8} = Res8, #{<<"metric">> := #{<<"one">> := _, <<"count">> := _} = IM2} = B8, ?assertEqual(2, maps:size(IM2)), ?assertEqual(1, maps:size(B8)), % Negative case for a non-existent (host type, metric) pair - Res9 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/host_type/", ExampleHostType, - "/nonExistentMetric"]), - ?PORT), + Res9 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType, "/nonExistentMetric"]), assert_status(404, Res9), % All global metrics - Res10 = simple_request(<<"GET">>, "/metrics/global", ?PORT), + Res10 = request(<<"GET">>, "/metrics/global"), {_, _, B10} = Res10, #{<<"metrics">> := _} = B10, ?assertEqual(1, maps:size(B10)), - Res11 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/global/", ExampleGlobal]), - ?PORT), + Res11 = request(<<"GET">>, ["/metrics/global/", ExampleGlobal]), {_, _, B11} = Res11, #{<<"metric">> := _} = B11, ?assertEqual(1, maps:size(B11)). @@ -394,28 +376,22 @@ fetch_counter_value(Counter, _Config) -> HostType = host_type(), HostTypeName = metrics_helper:make_host_type_name(HostType), - Result = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/host_type/", HostTypeName, "/", Metric]), - ?PORT), + Result = request(<<"GET">>, ["/metrics/host_type/", HostTypeName, "/", Metric]), {_S, _H, B} = Result, assert_status(200, Result), #{<<"metric">> := #{<<"count">> := HostTypeValue}} = B, - Result2 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/host_type/", HostTypeName]), - ?PORT), + Result2 = request(<<"GET">>, ["/metrics/host_type/", HostTypeName]), {_S2, _H2, B2} = Result2, assert_status(200, Result2), #{<<"metrics">> := #{Metric := #{<<"count">> := HostTypeValueList}}} = B2, - Result3 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/all/", Metric]), - ?PORT), + Result3 = request(<<"GET">>, ["/metrics/all/", Metric]), {_S3, _H3, B3} = Result3, assert_status(200, Result3), #{<<"metric">> := #{<<"count">> := TotalValue}} = B3, - Result4 = simple_request(<<"GET">>, "/metrics/all/", ?PORT), + Result4 = request(<<"GET">>, "/metrics/all/"), {_S4, _H4, B4} = Result4, assert_status(200, Result4), #{<<"metrics">> := #{Metric := #{<<"count">> := TotalValueList}}} = B4, @@ -434,8 +410,8 @@ fetch_global_gauge_value(Counter, Config) -> fetch_global_incrementing_gauge_value(Counter, Config) -> [Value, ValueList] = fetch_global_gauge_values(Counter, Config), ?assertEqual(true, Value =< ValueList, [{counter, Counter}, - {value, Value}, - {value_list, ValueList}]), + {value, Value}, + {value_list, ValueList}]), ValueList. fetch_global_gauge_values(Counter, Config) -> @@ -449,24 +425,19 @@ fetch_global_spiral_values(Counter, Config) -> fetch_global_counter_values(MetricKey, Counter, Config) -> Metric = atom_to_binary(Counter, utf8), - Port = case metrics_helper:all_metrics_are_global(Config) of - true -> - ct:get_config({hosts, mim2, metrics_rest_port}); - _ -> ct:get_config({hosts, mim, metrics_rest_port}) - end, + Server = case metrics_helper:all_metrics_are_global(Config) of + true -> mim2(); + _ -> mim() + end, - Result = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/global/", Metric]), - Port), + Result = request(<<"GET">>, ["/metrics/global/", Metric], Server), assert_status(200, Result), {_S, H, B} = Result, #{<<"metric">> := #{MetricKey := Value}} = B, ?assertEqual(<<"application/json">>, proplists:get_value(<<"content-type">>, H)), ?assertEqual(1, maps:size(B)), - Result2 = simple_request(<<"GET">>, - unicode:characters_to_list(["/metrics/global/"]), - Port), + Result2 = request(<<"GET">>, ["/metrics/global/"], Server), assert_status(200, Result2), {_S2, H2, B2} = Result2, ?assertEqual(<<"application/json">>, proplists:get_value(<<"content-type">>, H2)), @@ -502,3 +473,11 @@ ensure_nodes_clustered(Config) -> [distributed_helper:add_node_to_cluster(N, Config) || N <- NodesToBeClustered], Config. + +request(Method, Path) -> + make_request(#{role => admin, method => Method, path => iolist_to_binary(Path), + return_headers => true, return_maps => true}). + +request(Method, Path, Server) -> + make_request(#{role => admin, method => Method, path => iolist_to_binary(Path), + return_headers => true, return_maps => true, server => Server}). From 0879c5ed88530103cb299c3dae502dd5bd453059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:31:00 +0200 Subject: [PATCH 065/117] Remove the obsolete mongoose_api --- src/metrics/mongoose_api_metrics.erl | 172 -------------------- src/mongoose_api.erl | 235 --------------------------- src/mongoose_api_format.erl | 21 --- src/mongoose_api_json.erl | 92 ----------- src/mongoose_api_users.erl | 140 ---------------- src/mongoose_api_xml.erl | 65 -------- 6 files changed, 725 deletions(-) delete mode 100644 src/metrics/mongoose_api_metrics.erl delete mode 100644 src/mongoose_api.erl delete mode 100644 src/mongoose_api_format.erl delete mode 100644 src/mongoose_api_json.erl delete mode 100644 src/mongoose_api_users.erl delete mode 100644 src/mongoose_api_xml.erl diff --git a/src/metrics/mongoose_api_metrics.erl b/src/metrics/mongoose_api_metrics.erl deleted file mode 100644 index f5ee51443b7..00000000000 --- a/src/metrics/mongoose_api_metrics.erl +++ /dev/null @@ -1,172 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api_metrics). - --include("mongoose.hrl"). - -%% mongoose_api callbacks --export([prefix/0, - routes/0, - handle_options/2, - handle_get/2]). - -%% internal exports --export([available_metrics/1, - sum_metrics/1, - sum_metric/1, - host_type_metric/1, - host_type_metrics/1, - global_metric/1, - global_metrics/1 -]). - --ignore_xref([available_metrics/1, global_metric/1, global_metrics/1, handle_get/2, - handle_options/2, host_type_metric/1, host_type_metrics/1, prefix/0, - routes/0, sum_metric/1, sum_metrics/1]). - -%%-------------------------------------------------------------------- -%% mongoose_api callbacks -%%-------------------------------------------------------------------- --spec prefix() -> mongoose_api:prefix(). -prefix() -> - "/metrics". - --spec routes() -> mongoose_api:routes(). -routes() -> - [{"/", [available_metrics]}, - {"/all", [sum_metrics]}, - {"/all/:metric", [sum_metric]}, - {"/global", [global_metrics]}, - {"/global/:metric", [global_metric]}, - {"/host_type/:host_type/:metric", [host_type_metric]}, - {"/host_type/:host_type", [host_type_metrics]}]. - --spec handle_options(mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:methods(). -handle_options(_Bindings, [_Command]) -> - [get]. - --spec handle_get(mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:response(). -handle_get(Bindings, [Command]) -> - ?MODULE:Command(Bindings). - -%%-------------------------------------------------------------------- -%% mongoose_api commands actual handlers -%%-------------------------------------------------------------------- -available_metrics(_Bindings) -> - {HostTypes, Metrics} = get_available_host_type_metrics(), - Global = get_available_global_metrics(), - Reply = [{host_types, HostTypes}, {metrics, Metrics}, {global, Global}], - {ok, Reply}. - -sum_metrics(_Bindings) -> - Metrics = {metrics, get_sum_metrics()}, - {ok, Metrics}. - -sum_metric(Bindings) -> - {metric, Metric} = lists:keyfind(metric, 1, Bindings), - try - case get_sum_metric(binary_to_existing_atom(Metric, utf8)) of - [] -> - {error, not_found}; - Value -> - {ok, {metric, Value}} - end - catch error:badarg -> - {error, not_found} - end. - -host_type_metric(Bindings) -> - {host_type, HostType} = lists:keyfind(host_type, 1, Bindings), - {metric, Metric} = lists:keyfind(metric, 1, Bindings), - try - MetricAtom = binary_to_existing_atom(Metric, utf8), - {ok, Value} = mongoose_metrics:get_metric_value([HostType, MetricAtom]), - {ok, {metric, Value}} - catch error:badarg -> - {error, not_found} - end. - -host_type_metrics(Bindings) -> - {host_type, HostType} = lists:keyfind(host_type, 1, Bindings), - case get_host_type_metrics(HostType) of - [] -> - {error, not_found}; - Metrics -> - {ok, {metrics, Metrics}} - end. - -global_metric(Bindings) -> - {metric, Metric} = lists:keyfind(metric, 1, Bindings), - MetricAtom = binary_to_existing_atom(Metric, utf8), - case mongoose_metrics:get_metric_value(global, MetricAtom) of - {ok, Value} -> - {ok, {metric, Value}}; - _Other -> - {error, not_found} - end. - -global_metrics(_Bindings) -> - case get_host_type_metrics(global) of - [] -> - {error, not_found}; - Metrics -> - {ok, {metrics, Metrics}} - end. - - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- --spec get_available_host_types() -> [mongooseim:host_type()]. -get_available_host_types() -> - ?ALL_HOST_TYPES. - --spec get_available_metrics(HostType :: mongooseim:host_type()) -> [any()]. -get_available_metrics(HostType) -> - mongoose_metrics:get_host_type_metric_names(HostType). - --spec get_available_host_type_metrics() -> {[any(), ...], [any()]}. -get_available_host_type_metrics() -> - HostTypes = get_available_host_types(), - Metrics = [Metric || [Metric] <- get_available_metrics(hd(HostTypes))], - {HostTypes, Metrics}. - -get_available_global_metrics() -> - [Metric || [Metric] <- mongoose_metrics:get_global_metric_names()]. - --spec get_sum_metrics() -> [{_, _}]. -get_sum_metrics() -> - {_HostTypes, Metrics} = get_available_host_type_metrics(), - [{Metric, get_sum_metric(Metric)} || Metric <- Metrics]. - --spec get_sum_metric(atom()) -> [{_, _}]. -get_sum_metric(Metric) -> - mongoose_metrics:get_aggregated_values(Metric). - --spec get_host_type_metrics(undefined | global | mongooseim:host_type()) -> [{_, _}]. -get_host_type_metrics(HostType) -> - Metrics = mongoose_metrics:get_metric_values(HostType), - [{prep_name(NameParts), Value} || {[_HostType | NameParts], Value} <- Metrics]. - -prep_name(NameParts) -> - ToStrings = [part_to_string(NamePart) || NamePart <- NameParts], - string:join(ToStrings, "."). - -part_to_string(Part) when is_atom(Part) -> atom_to_list(Part); -part_to_string(Part) when is_binary(Part) -> binary_to_list(Part); -part_to_string(Part) -> Part. diff --git a/src/mongoose_api.erl b/src/mongoose_api.erl deleted file mode 100644 index b5c265d48c5..00000000000 --- a/src/mongoose_api.erl +++ /dev/null @@ -1,235 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api). - --behaviour(mongoose_http_handler). --behaviour(cowboy_rest). - -%% mongoose_http_handler callbacks --export([config_spec/0, routes/1]). - -%% cowboy_rest callbacks --export([init/2, - terminate/3, - allowed_methods/2, - content_types_provided/2, - content_types_accepted/2, - delete_resource/2]). - --export([to_xml/2, - to_json/2, - from_json/2]). - --ignore_xref([behaviour_info/1, cowboy_router_paths/2, from_json/2, to_json/2, to_xml/2]). - --record(state, {handler, opts, bindings}). - --type prefix() :: string(). --type route() :: {string(), options()}. --type routes() :: [route()]. --type bindings() :: proplists:proplist(). --type options() :: [any()]. --type method() :: get | post | put | patch | delete. --type methods() :: [method()]. --type response() :: ok | {ok, any()} | {error, atom()}. --export_type([prefix/0, routes/0, route/0, bindings/0, options/0, response/0, methods/0]). - --callback prefix() -> prefix(). --callback routes() -> routes(). --callback handle_options(bindings(), options()) -> methods(). --callback handle_get(bindings(), options()) -> response(). --callback handle_post(term(), bindings(), options()) -> response(). --callback handle_put(term(), bindings(), options()) -> response(). --callback handle_delete(bindings(), options()) -> response(). - --include("mongoose_config_spec.hrl"). - --type handler_options() :: #{path := string(), handlers := [module()], atom() => any()}. - -%%-------------------------------------------------------------------- -%% mongoose_http_handler callbacks -%%-------------------------------------------------------------------- --spec config_spec() -> mongoose_config_spec:config_section(). -config_spec() -> - HandlerModules = [mongoose_api_metrics, mongoose_api_users], - #section{items = #{<<"handlers">> => #list{items = #option{type = atom, - validate = {enum, HandlerModules}}, - validate = unique_non_empty} - }, - defaults = #{<<"handlers">> => HandlerModules}}. - --spec routes(handler_options()) -> mongoose_http_handler:routes(). -routes(#{path := BasePath, handlers := HandlerModules}) -> - lists:flatmap(fun(Module) -> cowboy_routes(BasePath, Module) end, HandlerModules). - -cowboy_routes(BasePath, Module) -> - [{[BasePath, Module:prefix(), Path], ?MODULE, #{module => Module, opts => Opts}} - || {Path, Opts} <- Module:routes()]. - -%%-------------------------------------------------------------------- -%% cowboy_rest callbacks -%%-------------------------------------------------------------------- -init(Req, #{module := Module, opts := Opts}) -> - Bindings = maps:to_list(cowboy_req:bindings(Req)), - State = #state{handler = Module, opts = Opts, bindings = Bindings}, - {cowboy_rest, Req, State}. % upgrade protocol - -terminate(_Reason, _Req, _State) -> - ok. - -allowed_methods(Req, #state{bindings=Bindings, opts=Opts}=State) -> - case call(handle_options, [Bindings, Opts], State) of - no_call -> - allowed_methods_from_exports(Req, State); - Methods -> - allowed_methods_from_module(Methods, Req, State) - end. - -content_types_provided(Req, State) -> - CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}, - {{<<"application">>, <<"xml">>, '*'}, to_xml}], - {CTP, Req, State}. - -content_types_accepted(Req, State) -> - CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], - {CTA, Req, State}. - -delete_resource(Req, State) -> - handle_delete(Req, State). - -%%-------------------------------------------------------------------- -%% content_types_provided/2 callbacks -%%-------------------------------------------------------------------- -to_json(Req, State) -> - handle_get(mongoose_api_json, Req, State). - -to_xml(Req, State) -> - handle_get(mongoose_api_xml, Req, State). - -%%-------------------------------------------------------------------- -%% content_types_accepted/2 callbacks -%%-------------------------------------------------------------------- -from_json(Req, State) -> - handle_unsafe(mongoose_api_json, Req, State). - -%%-------------------------------------------------------------------- -%% HTTP verbs handlers -%%-------------------------------------------------------------------- -handle_get(Serializer, Req, #state{opts=Opts, bindings=Bindings}=State) -> - Result = call(handle_get, [Bindings, Opts], State), - handle_result(Result, Serializer, Req, State). - -handle_unsafe(Deserializer, Req, State) -> - Method = cowboy_req:method(Req), - {ok, Body, Req1} = cowboy_req:read_body(Req), - case Deserializer:deserialize(Body) of - {ok, Data} -> - handle_unsafe(Method, Data, Req1, State); - {error, _Reason} -> - error_response(bad_request, Req1, State) - end. - -handle_unsafe(Method, Data, Req, #state{opts=Opts, bindings=Bindings}=State) -> - case method_callback(Method) of - not_implemented -> - error_response(not_implemented, Req, State); - Callback -> - Result = call(Callback, [Data, Bindings, Opts], State), - handle_result(Result, Req, State) - end. - -handle_delete(Req, #state{opts=Opts, bindings=Bindings}=State) -> - Result = call(handle_delete, [Bindings, Opts], State), - handle_result(Result, Req, State). - -%%-------------------------------------------------------------------- -%% Helpers -%%-------------------------------------------------------------------- -handle_result({ok, Result}, Serializer, Req, State) -> - serialize(Result, Serializer, Req, State); -handle_result(Other, _Serializer, Req, State) -> - handle_result(Other, Req, State). - -handle_result(ok, Req, State) -> - {true, Req, State}; -handle_result({error, Error}, Req, State) -> - error_response(Error, Req, State); -handle_result(no_call, Req, State) -> - error_response(not_implemented, Req, State). - -allowed_methods_from_module(Methods, Req, State) -> - Methods1 = case lists:member(get, Methods) of - true -> [head | Methods]; - false -> Methods - end, - Methods2 = [options | Methods1], - {methods_to_binary(Methods2), Req, State}. - -allowed_methods_from_exports(Req, #state{handler=Handler}=State) -> - Exports = Handler:module_info(exports), - Methods = lists:foldl(fun collect_allowed_methods/2, [options], Exports), - {methods_to_binary(Methods), Req, State}. - -collect_allowed_methods({handle_get, 2}, Acc) -> - [head, get | Acc]; -collect_allowed_methods({handle_post, 3}, Acc) -> - [post | Acc]; -collect_allowed_methods({handle_put, 3}, Acc) -> - [put | Acc]; -collect_allowed_methods({handle_delete, 2}, Acc) -> - [delete | Acc]; -collect_allowed_methods(_Other, Acc) -> - Acc. - -serialize(Data, Serializer, Req, State) -> - {Serializer:serialize(Data), Req, State}. - -call(Function, Args, #state{handler=Handler}) -> - try - apply(Handler, Function, Args) - catch error:undef -> - no_call - end. - -%%-------------------------------------------------------------------- -%% Error responses -%%-------------------------------------------------------------------- -error_response(Code, Req, State) when is_integer(Code) -> - Req1 = cowboy_req:reply(Code, Req), - {stop, Req1, State}; -error_response(Reason, Req, State) -> - error_response(error_code(Reason), Req, State). - -error_code(bad_request) -> 400; -error_code(not_found) -> 404; -error_code(conflict) -> 409; -error_code(unprocessable) -> 422; -error_code(not_implemented) -> 501. - -methods_to_binary(Methods) -> - [method_to_binary(Method) || Method <- Methods]. - -method_to_binary(get) -> <<"GET">>; -method_to_binary(post) -> <<"POST">>; -method_to_binary(put) -> <<"PUT">>; -method_to_binary(delete) -> <<"DELETE">>; -method_to_binary(patch) -> <<"PATCH">>; -method_to_binary(options) -> <<"OPTIONS">>; -method_to_binary(head) -> <<"HEAD">>. - -method_callback(<<"POST">>) -> handle_post; -method_callback(<<"PUT">>) -> handle_put; -method_callback(_Other) -> not_implemented. diff --git a/src/mongoose_api_format.erl b/src/mongoose_api_format.erl deleted file mode 100644 index 0b366f53811..00000000000 --- a/src/mongoose_api_format.erl +++ /dev/null @@ -1,21 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api_format). - --ignore_xref([behaviour_info/1]). - --callback serialize(term()) -> iodata(). --callback deserialize(iodata()) -> term(). diff --git a/src/mongoose_api_json.erl b/src/mongoose_api_json.erl deleted file mode 100644 index 4820a3cc52b..00000000000 --- a/src/mongoose_api_json.erl +++ /dev/null @@ -1,92 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api_json). - --behaviour(mongoose_api_format). - -%% mongoose_api_format callbacks --export([serialize/1, - deserialize/1]). - -%%-------------------------------------------------------------------- -%% mongoose_api_format callbacks -%%-------------------------------------------------------------------- -deserialize(Json) -> - try jiffy:decode(Json, [return_maps]) of - Data -> - {ok, do_deserialize(Data)} - catch _:_ -> - {error, unprocessable} - end. - -serialize(Data) -> - do_serialize(Data). - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- -do_deserialize(#{} = Map) -> - maps:to_list(maps:map(fun(_K, V) -> do_deserialize(V) end, Map)); -do_deserialize(NotAMap) -> - NotAMap. - -do_serialize(Data) -> - jiffy:encode(prepare_struct(Data)). - -prepare_struct({Key, Value}) -> - #{prepare_key(Key) => prepare_struct(Value)}; -prepare_struct([]) -> - []; -prepare_struct(List) when is_list(List) -> - case is_proplist(List) of - true -> - maps:from_list([{prepare_key(K), prepare_struct(V)} || {K, V} <- List]); - false -> - [prepare_struct(Element) || Element <- List] - end; -prepare_struct(List) when is_list(List) -> - try unicode:characters_to_binary(List) of - Bin when is_binary(Bin) -> Bin; - _ -> List %% Items in List are not valid unicode codepoints - catch - error:badarg -> List %% List is not a list of characters - end; -prepare_struct(Other) -> - Other. - -prepare_key(Key) when is_integer(Key) -> - integer_to_binary(Key); -prepare_key(Key) when is_list(Key); is_binary(Key) -> - case unicode:characters_to_binary(Key) of - Bin when is_binary(Bin) -> Bin - end; -prepare_key(Key) -> - Key. - -is_proplist(List) -> - is_proplist(List, sets:new()). - -is_proplist([], _Keys) -> - true; -is_proplist([{Key, _} | Tail], Keys) -> - case sets:is_element(Key, Keys) of - true -> - false; - false -> - is_proplist(Tail, sets:add_element(Key, Keys)) - end; -is_proplist(_Other, _Keys) -> - false. diff --git a/src/mongoose_api_users.erl b/src/mongoose_api_users.erl deleted file mode 100644 index 8df2ae5da61..00000000000 --- a/src/mongoose_api_users.erl +++ /dev/null @@ -1,140 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api_users). - -%% mongoose_api callbacks --export([prefix/0, - routes/0, - handle_options/2, - handle_get/2, - handle_put/3, - handle_delete/2]). - --ignore_xref([handle_delete/2, handle_get/2, handle_options/2, handle_put/3, - prefix/0, routes/0]). - --define(ERROR, {error, unprocessable}). - -%%-------------------------------------------------------------------- -%% mongoose_api callbacks -%%-------------------------------------------------------------------- --spec prefix() -> mongoose_api:prefix(). -prefix() -> - "/users". - --spec routes() -> mongoose_api:routes(). -routes() -> - [{"/host/:host", [host_users]}, - {"/host/:host/username/:username", [host_user]}]. - --spec handle_options(mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:methods(). -handle_options(_Bindings, [host_users]) -> - [get]; -handle_options(_Bindings, [host_user]) -> - [put, delete]. - --spec handle_get(mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:response(). -handle_get(Bindings, [host_users]) -> - get_users(Bindings). - --spec handle_put(term(), mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:response(). -handle_put(Data, Bindings, [host_user]) -> - put_user(Data, Bindings). - --spec handle_delete(mongoose_api:bindings(), mongoose_api:options()) -> - mongoose_api:response(). -handle_delete(Bindings, [host_user]) -> - delete_user(Bindings). - -%%-------------------------------------------------------------------- -%% mongoose_api commands actual handlers -%%-------------------------------------------------------------------- -get_users(Bindings) -> - Host = proplists:get_value(host, Bindings), - Users = ejabberd_auth:get_vh_registered_users(Host), - Response = [{count, length(Users)}, - {users, users_to_proplist(Users)}], - {ok, Response}. - -put_user(Data, Bindings) -> - Host = proplists:get_value(host, Bindings), - Username = proplists:get_value(username, Bindings), - case proplist_to_user(Data) of - {ok, Password} -> - maybe_register_user(Username, Host, Password); - {error, _} -> - ?ERROR - end. - -delete_user(Bindings) -> - Host = proplists:get_value(host, Bindings), - Username = proplists:get_value(username, Bindings), - JID = jid:make(Username, Host, <<>>), - case ejabberd_auth:does_user_exist(JID) of - true -> - maybe_delete_user(JID); - false -> - {error, not_found} - end. - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- -maybe_register_user(Username, Host, Password) -> - JID = jid:make(Username, Host, <<>>), - case ejabberd_auth:try_register(JID, Password) of - {error, not_allowed} -> - ?ERROR; - {error, exists} -> - maybe_change_password(JID, Password); - _ -> - ok - end. - -maybe_change_password(JID, Password) -> - case ejabberd_auth:set_password(JID, Password) of - {error, _} -> - ?ERROR; - ok -> - ok - end. - -maybe_delete_user(JID) -> - case ejabberd_auth:remove_user(JID) of - ok -> - ok; - _ -> - error - end. - -users_to_proplist(Users) -> - [user_to_proplist(User) || User <- Users]. - -user_to_proplist({Username, Host}) -> - {user, [{username, Username}, {host, Host}]}. - -proplist_to_user([{<<"user">>, User}]) -> - case proplists:get_value(<<"password">>, User) of - undefined -> - ?ERROR; - Password -> - {ok, Password} - end; -proplist_to_user(_Other) -> - ?ERROR. diff --git a/src/mongoose_api_xml.erl b/src/mongoose_api_xml.erl deleted file mode 100644 index 3952362aa51..00000000000 --- a/src/mongoose_api_xml.erl +++ /dev/null @@ -1,65 +0,0 @@ -%%============================================================================== -%% Copyright 2014 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(mongoose_api_xml). - --behaviour(mongoose_api_format). - --export([serialize/1, deserialize/1]). - --include("mongoose.hrl"). --include_lib("exml/include/exml.hrl"). - -%%-------------------------------------------------------------------- -%% mongoose_api_format callbacks -%%-------------------------------------------------------------------- -serialize(Data) -> - do_serialize(Data). - -deserialize(IOList) -> - {ok, ParsedXML} = exml:parse(iolist_to_binary([IOList])), - ParsedXML. - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- -do_serialize(Data) -> - exml:to_iolist(prepare_xmlel(Data)). - -prepare_xmlel(List) when is_list(List) -> - prepare_xmlel({<<"list">>, List}); -prepare_xmlel({ElementName, List}) when is_list(List) -> - {Attrs, Children} = lists:partition(fun is_attribute/1, List), - #xmlel{name = to_iolist_compliant(ElementName), - attrs = [prepare_xmlel(Attr) || Attr <- Attrs], - children = [prepare_xmlel(Child) || Child <- Children]}; -prepare_xmlel({Key, Value}) -> - {to_iolist_compliant(Key), to_iolist_compliant(Value)}; -prepare_xmlel(Other) -> - #xmlel{name = to_iolist_compliant(Other)}. - -is_attribute({_, List}) when is_list(List) -> - false; -is_attribute({_, _}) -> - true; -is_attribute(_) -> - false. - -to_iolist_compliant(Atom) when is_atom(Atom) -> - atom_to_binary(Atom, utf8); -to_iolist_compliant(Int) when is_integer(Int) -> - integer_to_binary(Int); -to_iolist_compliant(Other) -> - Other. From 2f17af3a11bac53bcd49f16ca9d9ed0afca322e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 09:32:09 +0200 Subject: [PATCH 066/117] Remove remaining references to the obsolete API --- src/metrics/mongoose_metrics.erl | 2 +- src/mongoose_http_handler.erl | 1 - src/system_metrics/mongoose_system_metrics_collector.erl | 3 +-- test/common/config_parser_helper.erl | 3 --- test/config_parser_SUITE.erl | 7 ------- test/mongoose_listener_SUITE_data/mongooseim.basic.toml | 4 ---- 6 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/metrics/mongoose_metrics.erl b/src/metrics/mongoose_metrics.erl index 37b01571aaa..393a4792675 100644 --- a/src/metrics/mongoose_metrics.erl +++ b/src/metrics/mongoose_metrics.erl @@ -46,7 +46,7 @@ -ignore_xref([get_dist_data_stats/0, get_mnesia_running_db_nodes_count/0, get_rdbms_data_stats/0, get_rdbms_data_stats/1, get_up_time/0, remove_host_type_metrics/1, get_report_interval/0, - sample_metric/1]). + sample_metric/1, get_metric_value/1]). -define(PREFIXES, mongoose_metrics_prefixes). -define(DEFAULT_REPORT_INTERVAL, 60000). %%60s diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index e1da3d77589..f318e2f84a6 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -84,5 +84,4 @@ configurable_handler_modules() -> [mod_websockets, mongoose_client_api, mongoose_admin_api, - mongoose_api, mongoose_graphql_cowboy_handler]. diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index b206be499be..4cca5f7e821 100644 --- a/src/system_metrics/mongoose_system_metrics_collector.erl +++ b/src/system_metrics/mongoose_system_metrics_collector.erl @@ -126,8 +126,7 @@ get_api() -> [#{report_name => http_api, key => Api, value => enabled} || Api <- ApiList]. filter_unknown_api(ApiList) -> - AllowedToReport = [mongoose_api, mongoose_client_api, mongoose_admin_api, - mod_bosh, mod_websockets], + AllowedToReport = [mongoose_client_api, mongoose_admin_api, mod_bosh, mod_websockets], [Api || Api <- ApiList, lists:member(Api, AllowedToReport)]. get_transport_mechanisms() -> diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 81dde621358..8e0558d25e8 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -1081,9 +1081,6 @@ default_config([listen, http, handlers, mongoose_client_api]) -> mongoose_client_api_rooms_messages], docs => true, module => mongoose_client_api}; -default_config([listen, http, handlers, mongoose_api]) -> - #{handlers => [mongoose_api_metrics, mongoose_api_users], - module => mongoose_api}; default_config([listen, http, handlers, mongoose_graphql_cowboy_handler]) -> #{module => mongoose_graphql_cowboy_handler, schema_endpoint => admin}; diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 5a9dfaa90ec..51f1ba5ba75 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -98,7 +98,6 @@ groups() -> listen_http_handlers_websockets, listen_http_handlers_client_api, listen_http_handlers_admin_api, - listen_http_handlers_api, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, auth_password, @@ -606,12 +605,6 @@ listen_http_handlers_admin_api(_Config) -> {P, T} = test_listen_http_handler(mongoose_admin_api), test_listen_http_handler_creds(P, T). -listen_http_handlers_api(_Config) -> - {P, T} = test_listen_http_handler(mongoose_api), - ?cfg(P ++ [handlers], [mongoose_api_metrics], - T(#{<<"handlers">> => [<<"mongoose_api_metrics">>]})), - ?err(T(#{<<"handlers">> => [not_a_module]})). - listen_http_handlers_graphql(_Config) -> T = fun graphql_handler_raw/1, {P, _} = test_listen_http_handler(mongoose_graphql_cowboy_handler, T), diff --git a/test/mongoose_listener_SUITE_data/mongooseim.basic.toml b/test/mongoose_listener_SUITE_data/mongooseim.basic.toml index ccd65946f54..5e0533f8f07 100644 --- a/test/mongoose_listener_SUITE_data/mongooseim.basic.toml +++ b/test/mongoose_listener_SUITE_data/mongooseim.basic.toml @@ -13,10 +13,6 @@ host = "_" path = "/ws-xmpp" - [[listen.http.handlers.mongoose_api]] - host = "localhost" - path = "/api" - [[listen.c2s]] port = 5222 From 6e56f0d67a6f89bea1c86dabfd8981b4e213e11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 10:59:04 +0200 Subject: [PATCH 067/117] Remove tests for non-existing old user API --- big_tests/default.spec | 1 - big_tests/dynamic_domains.spec | 1 - big_tests/tests/users_api_SUITE.erl | 160 ---------------------------- 3 files changed, 162 deletions(-) delete mode 100644 big_tests/tests/users_api_SUITE.erl diff --git a/big_tests/default.spec b/big_tests/default.spec index 17ed70b62db..e8e862774c8 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -100,7 +100,6 @@ {suites, "tests", sic_SUITE}. {suites, "tests", smart_markers_SUITE}. {suites, "tests", sm_SUITE}. -{suites, "tests", users_api_SUITE}. {suites, "tests", vcard_SUITE}. {suites, "tests", vcard_simple_SUITE}. {suites, "tests", websockets_SUITE}. diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index 9346d9d0cd7..f83e7ed9e60 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -148,7 +148,6 @@ {suites, "tests", smart_markers_SUITE}. {suites, "tests", sm_SUITE}. -{suites, "tests", users_api_SUITE}. {suites, "tests", vcard_SUITE}. {suites, "tests", vcard_simple_SUITE}. {suites, "tests", websockets_SUITE}. diff --git a/big_tests/tests/users_api_SUITE.erl b/big_tests/tests/users_api_SUITE.erl deleted file mode 100644 index 9c3599217ec..00000000000 --- a/big_tests/tests/users_api_SUITE.erl +++ /dev/null @@ -1,160 +0,0 @@ -%%============================================================================== -%% Copyright 2020 Erlang Solutions Ltd. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%============================================================================== --module(users_api_SUITE). --compile([export_all, nowarn_export_all]). - --include_lib("eunit/include/eunit.hrl"). - --import(distributed_helper, [mim/0, - require_rpc_nodes/1, - rpc/4]). --import(rest_helper, [assert_status/2, simple_request/2, simple_request/3, simple_request/4]). --import(domain_helper, [domain/0]). --import(mongoose_helper, [auth_modules/0]). - --define(DOMAIN, (domain())). --define(PORT, (ct:get_config({hosts, mim, metrics_rest_port}))). --define(USERNAME, "http_guy"). - -%%-------------------------------------------------------------------- -%% Suite configuration -%%-------------------------------------------------------------------- - -all() -> - [{group, transaction}, - {group, negative}]. - -groups() -> - G = [{transaction, [{repeat_until_any_fail, 10}], [user_transaction]}, - {negative, [], negative_calls_test_cases()} - ], - ct_helper:repeat_all_until_all_ok(G). - -negative_calls_test_cases() -> - [ - add_malformed_user, - add_user_without_proper_fields, - delete_non_existent_user - ]. - -init_per_suite(_Config) -> - case is_external_auth() of - true -> - {skip, "users api not compatible with external authentication"}; - false -> - [{riak_auth, is_riak_auth()}] - end. - -end_per_suite(_Config) -> - ok. - -suite() -> - require_rpc_nodes([mim]). - -%%-------------------------------------------------------------------- -%% users_api tests -%%-------------------------------------------------------------------- - -user_transaction(Config) -> - Count1 = fetch_list_of_users(Config), - add_user(?USERNAME, <<"my_http_password">>), - Count2 = fetch_list_of_users(Config), - % add user again = change their password - % check idempotence - add_user(?USERNAME, <<"some_other_password">>), - Count2 = fetch_list_of_users(Config), - add_user("http_guy2", <<"my_http_password">>), - Count3 = fetch_list_of_users(Config), - delete_user(?USERNAME), - delete_user("http_guy2"), - Count1 = fetch_list_of_users(Config), - - ?assertEqual(Count2, Count1+1), - ?assertEqual(Count3, Count2+1), - - wait_for_user_removal(proplists:get_value(riak_auth, Config)). - -add_malformed_user(_Config) -> - Path = unicode:characters_to_list(["/users/host/", ?DOMAIN, "/username/" ?USERNAME]), - % cannot use jiffy here, because the JSON is malformed - Res = simple_request(<<"PUT">>, Path, ?PORT, - <<"{ - \"user\": { - \"password\": \"my_http_password\" - }">>), - assert_status(400, Res). - -add_user_without_proper_fields(_Config) -> - Path = unicode:characters_to_list(["/users/host/", ?DOMAIN, "/username/" ?USERNAME]), - Body = jiffy:encode(#{<<"user">> => #{<<"pazzwourd">> => <<"my_http_password">>}}), - Res = simple_request(<<"PUT">>, Path, ?PORT, Body), - assert_status(422, Res). - -delete_non_existent_user(_Config) -> - Path = unicode:characters_to_list(["/users/host/", ?DOMAIN, "/username/i_don_exist"]), - Res = simple_request(<<"DELETE">>, Path, ?PORT), - assert_status(404, Res). - -%%-------------------------------------------------------------------- -%% internal functions -%%-------------------------------------------------------------------- - -fetch_list_of_users(_Config) -> - Result = simple_request(<<"GET">>, unicode:characters_to_list(["/users/host/", ?DOMAIN]), ?PORT), - assert_status(200, Result), - {_S, H, B} = Result, - ?assertEqual(<<"application/json">>, proplists:get_value(<<"content-type">>, H)), - #{<<"count">> := Count, <<"users">> := _} = B, - ?assertEqual(2, maps:size(B)), - Count. - -add_user(UserName, Password) -> - Path = unicode:characters_to_list(["/users/host/", ?DOMAIN, "/username/", UserName]), - Body = jiffy:encode(#{<<"user">> => #{<<"password">> => Password}}), - Res = simple_request(<<"PUT">>, Path, ?PORT, Body), - assert_status(204, Res), - Res. - -delete_user(UserName) -> - Path = unicode:characters_to_list(["/users/host/", ?DOMAIN, "/username/", UserName]), - Res = simple_request(<<"DELETE">>, Path, ?PORT), - assert_status(204, Res), - Res. - -is_external_auth() -> - lists:member(ejabberd_auth_external, auth_modules()). - -is_riak_auth() -> - lists:member(ejabberd_auth_riak, auth_modules()). - -wait_for_user_removal(false) -> - ok; -wait_for_user_removal(_) -> - Domain = domain(), - try mongoose_helper:wait_until( - fun() -> - rpc(mim(), ejabberd_auth_riak, get_vh_registered_users_number, [Domain]) - end, - 0, - #{ time_sleep => 500, time_left => 5000, name => rpc}) - of - {ok, 0} -> - ok - catch - _Error:Reason -> - ct:pal("~p", [Reason]), - ok - end. From 4440aab942d16e2a01cfaf62dd2b4285f3023ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 13:54:38 +0200 Subject: [PATCH 068/117] Add missing error tests and reorganize them --- big_tests/tests/metrics_api_SUITE.erl | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/big_tests/tests/metrics_api_SUITE.erl b/big_tests/tests/metrics_api_SUITE.erl index 7f2b0da9c24..b07805052ed 100644 --- a/big_tests/tests/metrics_api_SUITE.erl +++ b/big_tests/tests/metrics_api_SUITE.erl @@ -47,12 +47,11 @@ all() -> groups() -> [ - {metrics, [], ?METRICS_CASES}, + {metrics, [], [non_existent_metrics | ?METRICS_CASES]}, {all_metrics_are_global, [], ?METRICS_CASES}, {global, [], [session_counters, node_uptime, - cluster_size - ]} + cluster_size]} ]. init_per_suite(Config) -> @@ -87,6 +86,19 @@ end_per_testcase(CaseName, Config) -> %% metrics_api tests %%-------------------------------------------------------------------- +non_existent_metrics(_Config) -> + IncompleteName = "backends", + GlobalMetricName = "adhoc_local_commands", + HostType = metrics_helper:make_host_type_name(host_type()), + assert_status(404, request(<<"GET">>, "/metrics/all/" ++ IncompleteName)), + assert_status(404, request(<<"GET">>, "/metrics/all/badMetric")), + assert_status(404, request(<<"GET">>, "/metrics/global/" ++ IncompleteName)), + assert_status(404, request(<<"GET">>, "/metrics/global/badMetric")), + assert_status(404, request(<<"GET">>, "/metrics/host_type/badHostType")), + assert_status(404, request(<<"GET">>, "/metrics/host_type/badHostType/xmppStanzaCount")), + assert_status(404, request(<<"GET">>, ["/metrics/", HostType, "/", GlobalMetricName])), + assert_status(404, request(<<"GET">>, ["/metrics/", HostType, "/badMetric"])). + message_flow(Config) -> case metrics_helper:all_metrics_are_global(Config) of true -> metrics_only_global(Config); @@ -304,20 +316,12 @@ metrics_msg_flow(_Config) -> ?assertEqual(2, maps:size(IM)), ?assertEqual(1, maps:size(B4)), - % Negative case for a non-existent given metric - Res5 = request(<<"GET">>, "/metrics/all/nonExistentMetric"), - assert_status(404, Res5), - % All metrics for an example host type Res6 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType]), {_S6, _H6, B6} = Res6, #{<<"metrics">> := _} = B6, ?assertEqual(1, maps:size(B6)), - % Negative case for a non-existent host type - Res7 = request(<<"GET">>, "/metrics/host_type/nonExistentHostType"), - assert_status(404, Res7), - % An example metric for an example host type Res8 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType, "/", ExampleMetric]), {_S8, _H8, B8} = Res8, @@ -325,10 +329,6 @@ metrics_msg_flow(_Config) -> ?assertEqual(2, maps:size(IM2)), ?assertEqual(1, maps:size(B8)), - % Negative case for a non-existent (host type, metric) pair - Res9 = request(<<"GET">>, ["/metrics/host_type/", ExampleHostType, "/nonExistentMetric"]), - assert_status(404, Res9), - % All global metrics Res10 = request(<<"GET">>, "/metrics/global"), {_, _, B10} = Res10, From 26799334546461517de21bf87e38ac9739c1a9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Fri, 23 Sep 2022 14:46:51 +0200 Subject: [PATCH 069/117] Fix an invalid metric name It was revealed by the metric API tests. --- src/mam/mod_mam_pm.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mam/mod_mam_pm.erl b/src/mam/mod_mam_pm.erl index 9b2a0eddf24..dfc05f52ff7 100644 --- a/src/mam/mod_mam_pm.erl +++ b/src/mam/mod_mam_pm.erl @@ -717,7 +717,7 @@ remove_iq_handlers(HostType) -> ensure_metrics(HostType) -> mongoose_metrics:ensure_metric(HostType, [backends, ?MODULE, lookup], histogram), - mongoose_metrics:ensure_metric(HostType, [HostType, modMamLookups, simple], spiral), + mongoose_metrics:ensure_metric(HostType, [modMamLookups, simple], spiral), mongoose_metrics:ensure_metric(HostType, [backends, ?MODULE, archive], histogram), lists:foreach(fun(Name) -> mongoose_metrics:ensure_metric(HostType, Name, spiral) From fed9dcb5bf8d9425dd2f00d247471b5f1f5faa18 Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Fri, 23 Sep 2022 16:48:17 +0200 Subject: [PATCH 070/117] Documented missing GraphQL types --- .../schemas/admin/admin_auth_status.gql | 6 +- priv/graphql/schemas/admin/metric.gql | 14 ++- priv/graphql/schemas/admin/mnesia.gql | 9 ++ priv/graphql/schemas/admin/server.gql | 17 +++ priv/graphql/schemas/global/muc.gql | 72 +++++++++++++ priv/graphql/schemas/global/muc_light.gql | 27 +++++ priv/graphql/schemas/global/roster.gql | 8 +- priv/graphql/schemas/global/scalar_types.gql | 17 +-- priv/graphql/schemas/global/spectaql_dir.gql | 2 + priv/graphql/schemas/global/stanza.gql | 10 ++ priv/graphql/schemas/global/token.gql | 3 + priv/graphql/schemas/global/vcard.gql | 101 +++++++++++++++++- 12 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 priv/graphql/schemas/global/spectaql_dir.gql diff --git a/priv/graphql/schemas/admin/admin_auth_status.gql b/priv/graphql/schemas/admin/admin_auth_status.gql index d59db22647a..799ab3aa0a6 100644 --- a/priv/graphql/schemas/admin/admin_auth_status.gql +++ b/priv/graphql/schemas/admin/admin_auth_status.gql @@ -9,10 +9,10 @@ type AdminAuthInfo{ } enum AuthType{ - "" + "Administrator of the specific domain" DOMAIN_ADMIN - "" + "Global administrator" ADMIN - "" + "Unauthorized user" UNAUTHORIZED } diff --git a/priv/graphql/schemas/admin/metric.gql b/priv/graphql/schemas/admin/metric.gql index 27b8a20673d..6c49e7fad6f 100644 --- a/priv/graphql/schemas/admin/metric.gql +++ b/priv/graphql/schemas/admin/metric.gql @@ -1,19 +1,31 @@ """ Result of a metric """ - enum MetricType { + "Collects values over a sliding window of 60s and returns apropriate statistical values" histogram + "Returns a number" counter + """ + Provides 2 values: total event count and a value in 60s window. + Dividing one value by 60 provides an average per-second value over last minute. + """ spiral + "Consists of value and time in milliseconds elapsed from the last metric update" gauge + "Informations about the inet" merged_inet_stats + "Metrics of the relational database management sytem" rdbms_stats + "Metrics of the virual machine memory" vm_stats_memory + "Information about virual machine" vm_system_info + "Information about process queue length" probe_queues } +"Type of metric result" union MetricResult = HistogramMetric | CounterMetric | SpiralMetric | GaugeMetric | MergedInetStatsMetric | RDBMSStatsMetric | VMStatsMemoryMetric | VMSystemInfoMetric diff --git a/priv/graphql/schemas/admin/mnesia.gql b/priv/graphql/schemas/admin/mnesia.gql index e08b17ad107..85afbac760a 100644 --- a/priv/graphql/schemas/admin/mnesia.gql +++ b/priv/graphql/schemas/admin/mnesia.gql @@ -40,17 +40,26 @@ type MnesiaAdminMutation @protected{ union MnesiaInfo = MnesiaStringResponse | MnesiaListResponse | MnesiaIntResponse +"Mnesia response in the form of a string" type MnesiaStringResponse { + "Result as a string" result: String + "Result's key" key: String } +"Mnesia response in the form of a list" type MnesiaListResponse { + "Result as a list" result: [String] + "Result's key" key: String } +"Mnesia response in the form of a integer" type MnesiaIntResponse { + "Result as an integer" result: Int + "Result's key" key: String } diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index c3c430a5b4c..353ca248c21 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -40,25 +40,42 @@ type ServerAdminMutation @protected{ @protected(type: GLOBAL) } +"Status of the server" type Status { + "Code of the status" statusCode: StatusCode + "Message about the status" message: String } +"Specifies status of the server" enum StatusCode { + "Server is running" RUNNING + "Server is not running" NOT_RUNNING } +"Logs events that equally or more severe than the configured level" enum LogLevel { + "Do not log any events" NONE + "Log when system is unusable" EMERGENCY + "Log when action must be taken immediately" ALERT + "Log critical conditions" CRITICAL + "Log error conditions" ERROR + "Log warning conditions" WARNING + "Log normal but significant conditions" NOTICE + "Long informational messages" INFO + "Log debug messages" DEBUG + "Log everything" ALL } diff --git a/priv/graphql/schemas/global/muc.gql b/priv/graphql/schemas/global/muc.gql index 16de7c3e350..d14a32ac348 100644 --- a/priv/graphql/schemas/global/muc.gql +++ b/priv/graphql/schemas/global/muc.gql @@ -1,87 +1,159 @@ +"User affilation to a specific room" enum MUCAffiliation{ + "The user is the owner of the room" OWNER + "The user has administrative role" ADMIN + "The user is a member of the rooom" MEMBER + "The user isn't a member of the room" OUTCAST + "The user doesn't have any affiliation" NONE } +"MUC role types" enum MUCRole{ + "User is a visiting" VISITOR + "User can participate in the room" PARTICIPANT + "User has ability to moderate room" MODERATOR } +"MUC room user data" type MUCRoomUser{ + "User's JID" jid: JID + "User's nickname" nick: String! + "User's role" role: MUCRole! } +"MUC room affiliation data" type MUCRoomAffiliation{ + "Room's JID" jid: JID! + "Affiliation type" affiliation: MUCAffiliation! } +"MUC room description" type MUCRoomDesc{ + "Room's JID" jid: JID! + "Room's title" title: String! + "Is room private?" private: Boolean + "Number of the users" usersNumber: Int } +"MUC room configuration" type MUCRoomConfig{ + "Room's title" title: String!, + "Room's description" description: String!, + "Allow to change room's subject?" allowChangeSubject: Boolean!, + "Allow to query users?" allowQueryUsers: Boolean!, + "Allow private messages?" allowPrivateMessages: Boolean!, + "Allow visitor staus?" allowVisitorStatus: Boolean!, + "Allow visitors to change their nicks?" allowVisitorNickchange: Boolean!, + "Is the room public?" public: Boolean!, + "Is the room on the public list?" publicList: Boolean!, + "Is the room persistent" persistent: Boolean!, + "Is the room moderated?" moderated: Boolean!, + "Should all new occupants be members by default?" membersByDefault: Boolean!, + "Should only users with member affiliation be allowed to join the room?" membersOnly: Boolean!, + "Can users invite others to join the room?" allowUserInvites: Boolean!, + "Allow multiple sessions of the room?" allowMultipleSession: Boolean!, + "Is the room password protected?" passwordProtected: Boolean!, + "Password to the room" password: String!, + "Are occupants, except from moderators, able see each others real JIDs?" anonymous: Boolean!, + "Array of roles and/or privileges that enable retrieving the room's member list" mayGetMemberList: [String!]! + "Maximum number of users in the room" maxUsers: Int, + "Does the room enabled logging events to a file on the disk?" logging: Boolean!, } +"MUC rooom configuration input" input MUCRoomConfigInput{ + "Room's title" title: String, + "Room's description" description: String, + "Allow to change room's subject?" allowChangeSubject: Boolean, + "Allow to query users?" allowQueryUsers: Boolean, + "Allow private messages?" allowPrivateMessages: Boolean, + "Allow visitor staus?" allowVisitorStatus: Boolean, + "Allow visitors to change their nicks?" allowVisitorNickchange: Boolean, + "Is the room public?" public: Boolean, + "Is the room on the public list?" publicList: Boolean, + "Is the room persistent" persistent: Boolean, + "Is the room moderated?" moderated: Boolean, + "Should all new occupants be members by default?" membersByDefault: Boolean, + "Should only users with member affiliation be allowed to join the room?" membersOnly: Boolean, + "Can users invite others to join the room?" allowUserInvites: Boolean, + "Allow multiple sessions of the room?" allowMultipleSession: Boolean, + "Is the room password protected?" passwordProtected: Boolean, + "Password to the room" password: String, + "Are occupants, except from moderators, able see each others real JIDs?" anonymous: Boolean, + "Array of roles and/or privileges that enable retrieving the room's member list" mayGetMemberList: [String!], + "Maximum number of users in the room" maxUsers: Int + "Does the room enabled logging events to a file on the disk?" logging: Boolean, } +"MUC rooms payload" type MUCRoomsPayload{ + "List of rooms descriptions" rooms: [MUCRoomDesc!] + "Number of the rooms" count: Int + "Index of the room" index: Int + "First room title" first: String + "Last room title" last: String } diff --git a/priv/graphql/schemas/global/muc_light.gql b/priv/graphql/schemas/global/muc_light.gql index a89fe1f539b..8c03627f317 100644 --- a/priv/graphql/schemas/global/muc_light.gql +++ b/priv/graphql/schemas/global/muc_light.gql @@ -1,28 +1,46 @@ +"User's affiliation" enum Affiliation{ + "Owner of the room" OWNER + "Member of the room" MEMBER + "User doesn't have any affiliation" NONE } +"Specifies blocking data" input BlockingInput{ + "Type of entity to block" entityType: BlockedEntityType! + "Type of blocking action" action: BlockingAction! + "Entity's JID" entity: JID! } +"Blocking item data" type BlockingItem{ + "Type of the entity" entityType: BlockedEntityType! + "Action to be taken" action: BlockingAction! + "Entity's JID" entity: JID! } +"Type of blocking action" enum BlockingAction{ + "Unblock user/room" ALLOW, + "Block user/room" DENY } +"Type of blocked entity" enum BlockedEntityType{ + "Individual user" USER, + "MUC Light room" ROOM } @@ -40,15 +58,24 @@ type RoomConfigDictEntry{ value: String! } +"Room data" type Room{ + "Room's JId" jid: JID! + "Name of the room" name: String! + "Subject of the room" subject: String! + "List of participants" participants: [RoomUser!]! + "Configuration options" options: [RoomConfigDictEntry!]! } +"Room user data" type RoomUser{ + "User's JID" jid: JID! + "User's affiliation" affiliation: Affiliation! } diff --git a/priv/graphql/schemas/global/roster.gql b/priv/graphql/schemas/global/roster.gql index b39f4c2fae8..9dc922c60ea 100644 --- a/priv/graphql/schemas/global/roster.gql +++ b/priv/graphql/schemas/global/roster.gql @@ -17,7 +17,7 @@ type Contact{ "The list of the groups the contact belongs to" groups: [String!] "The type of the subscription" - subscription: ContactSub + subscription: ContactSub "The type of the ask" ask: ContactAsk } @@ -45,11 +45,17 @@ enum ContactSub{ "The contact ask types" enum ContactAsk{ + "Ask to subscribe" SUBSCRIBE + "Ask to unsubscribe" UNSUBSCRIBE + "Invitation came in" IN + "Invitation came out" OUT + "Ask for mutual subscription" BOTH + "No invitation" NONE } diff --git a/priv/graphql/schemas/global/scalar_types.gql b/priv/graphql/schemas/global/scalar_types.gql index 4ee27eb6510..6b8d81bad48 100644 --- a/priv/graphql/schemas/global/scalar_types.gql +++ b/priv/graphql/schemas/global/scalar_types.gql @@ -1,7 +1,12 @@ +"Date and time represented using **YYYY-MM-DDTHH:mm:ssZ** format" scalar DateTime -scalar Stanza -scalar JID -"The JID with resource e.g. alice@localhost/res1" -scalar FullJID -scalar NonEmptyString -scalar PosInt +"Body of a data structure exchanged by XMPP entities in XML streams" +scalar Stanza @spectaql(options: [{ key: "example", value: "Hi!" }]) +"Unique indetifier in the form of **node@domain**" +scalar JID @spectaql(options: [{ key: "example", value: "alice@localhost" }]) +"The JID with resource" +scalar FullJID @spectaql(options: [{ key: "example", value: "alice@localhost/res1" }]) +"String that contains at least one character" +scalar NonEmptyString @spectaql(options: [{ key: "example", value: "xyz789" }]) +"Integer that has a value above zero" +scalar PosInt @spectaql(options: [{ key: "example", value: "2" }]) diff --git a/priv/graphql/schemas/global/spectaql_dir.gql b/priv/graphql/schemas/global/spectaql_dir.gql new file mode 100644 index 00000000000..d782ce34931 --- /dev/null +++ b/priv/graphql/schemas/global/spectaql_dir.gql @@ -0,0 +1,2 @@ +"Used to provide examples for non-standard types" +directive @spectaql(options: [SpectaQLOption]) on OBJECT | FIELD_DEFINITION | SCALAR diff --git a/priv/graphql/schemas/global/stanza.gql b/priv/graphql/schemas/global/stanza.gql index 1df83ce3d18..d015d1aa463 100644 --- a/priv/graphql/schemas/global/stanza.gql +++ b/priv/graphql/schemas/global/stanza.gql @@ -1,15 +1,25 @@ +"Stanza payload data" type StanzasPayload{ + "List of stanza's maps" stanzas: [StanzaMap] + "Max number of stanzas" limit: Int } +"Stanza map data" type StanzaMap{ + "Sender's JID" sender: JID + "Stanza's timestamp" timestamp: DateTime + "ID of the stanza" stanza_id: String + "Stanza's data" stanza: Stanza } +"Send stanza payload" type SendStanzaPayload{ + "Send stanza id" id: ID } diff --git a/priv/graphql/schemas/global/token.gql b/priv/graphql/schemas/global/token.gql index b2af0f7f7a8..3b9ef7b3ab8 100644 --- a/priv/graphql/schemas/global/token.gql +++ b/priv/graphql/schemas/global/token.gql @@ -1,4 +1,7 @@ +"Token data" type Token { + "Access token data" access: String + "Refresh token data" refresh: String } diff --git a/priv/graphql/schemas/global/vcard.gql b/priv/graphql/schemas/global/vcard.gql index 30810d747b4..b6a54979892 100644 --- a/priv/graphql/schemas/global/vcard.gql +++ b/priv/graphql/schemas/global/vcard.gql @@ -9,22 +9,35 @@ type Vcard{ photo: [Image] "Birthday date" birthday: [String] + "User's addresses" address: [Address] + "Formatted text corresponding to delivery address" label: [Label] + "User's telephone number" telephone: [Telephone] + "User's email" email: [Email] + "User's JID" jabberId: [String] + "User's mail agent type" mailer: [String] + "User's timezone" timeZone: [String] "Geographical position" geo: [GeographicalPosition] + "Job title, functional position or function" title: [String] + "User's role, occupation, or business category" role: [String] + "Logo image" logo: [Image] + "Person who will act on behalf of the user or resource associated with the vCard" agent: [Agent] - "Organization" + "Organizational name and units associated" org: [Organization] + "Application specific category information" categories: [Keyword] + "Note about user" note: [String] "Identifier of product that generated the vCard property" prodId: [String] @@ -47,14 +60,20 @@ type Vcard{ } type Keyword{ + "Keywords list" keyword: [String] } type NameComponents{ + "User's family name" family: String + "User's name" givenName: String + "User's middle name" middleName: String + "Prefix to the name" prefix: String + "Suffix to the name" suffix: String } @@ -65,23 +84,29 @@ type Address{ pobox: String "Extra address data" extadd: String + "Street name" street: String + "Locality (e.g. city)" locality: String + "Region name" region: String "Postal code" pcode: String + "Country name" country: String } type Label{ "Label tags" tags: [AddressTags] + "Individual label lines" line: [String] } type Telephone{ "Telephone tags" tags: [TelephoneTags] + "Telephone's number" number: String } @@ -93,30 +118,40 @@ type Email{ } type ImageData{ + "Format type parameter" type: String + "Base64 encoded binary image" binValue: String } type External{ + "URI to an external value" extValue: String } type Phonetic { + "Textual phonetic pronunciation" phonetic: String } type BinValue{ + "Value in binary form" binValue: String } +"Agent vCard" type AgentVcard{ + "vCard data" vcard: Vcard } +"Specifies how image is stored" union Image = ImageData | External +"Specifies how sound is stored" union Sound = Phonetic | BinValue | External +"Specifies how agent is stored" union Agent = AgentVcard | External type GeographicalPosition{ @@ -134,11 +169,14 @@ type Organization{ } type Privacy{ + "List of privacy classification tags" tags: [PrivacyClassificationTags] } type Key{ + "Type of a key" type: String + "Key credential" credential: String } @@ -153,22 +191,35 @@ input VcardInput{ photo: [ImageInput!] "Birthday date" birthday: [String!] + "User's address" address: [AddressInput!] + "Formatted text corresponding to delivery address" label: [LabelInput!] + "User's telephone number" telephone: [TelephoneInput!] + "User's email" email: [EmailInput!] + "User's JID" jabberId: [String!] + "User's mail agent type" mailer: [String!] + "User's timezone" timeZone: [String!] "Geographical position" geo: [GeographicalPositionInput!] + "Job title, functional position or function" title: [String!] + "User's role, occupation, or business category" role: [String!] + "Logo image" logo: [ImageInput!] + "Person who will act on behalf of the user or resource associated with the vCard" agent: [AgentInput!] - "Organization" + "Organizational name and units associated" org: [OrganizationInput!] + "Application specific category information" categories: [KeywordInput!] + "Note about user" note: [String!] "Identifier of product that generated the vCard property" prodId: [String!] @@ -191,14 +242,20 @@ input VcardInput{ } input KeywordInput{ + "Keywords list" keyword: [String!] } input NameComponentsInput{ + "User's family name" family: String + "User's name" givenName: String + "User's middle name" middleName: String + "Prefix to the name" prefix: String + "Suffix to the name" suffix: String } @@ -209,23 +266,29 @@ input AddressInput{ pobox: String "Extra address data" extadd: String + "Street name" street: String + "Locality (e.g. city)" locality: String + "Region name" region: String "Postal code" pcode: String + "Country name" country: String } input LabelInput{ "Label tags" tags: [AddressTags!] + "Individual label lines" line: [String!]! } input TelephoneInput{ "Telephone tags" tags: [TelephoneTags!] + "Telephone's number" number: String! } @@ -251,6 +314,7 @@ input OrganizationInput{ } input PrivacyInput{ + "Privacy classification tag list" tags: [PrivacyClassificationTags!] } @@ -280,46 +344,79 @@ input AgentInput{ } input KeyInput{ + "Type of input" type: String + "Credential or encryption key" credential: String! } enum PrivacyClassificationTags{ + "vCard may be shared with everyone" PUBLIC + "vCard will not be shared" PRIVATE + "vCard may be shared with allowed users" CONFIDENTIAL } +"Specifies type of an address" enum AddressTags{ + "Place of residence address" HOME + "Workplace adress" WORK + "Postal code" POSTAL + "Parcel delivery address" PARCEL + "Domestic delivery address" DOM + "Preferred delivery address when more than one address is specified" PREF + "International delivery address" INTL } +"Specifies intended use of a telphone number" enum TelephoneTags{ + "Number associated with a residence" HOME + "Number associated with a workplace" WORK + "Voice telephone number" VOICE + "Facsimile telephone number" FAX + "Paging device telephone number" PAGER + "Number has voice messaging support" MSG + "Cellular telephone number" CELL + "Video conferencing telephone number" VIDEO + "Bulletin board system telephone number" BBS + "Modem connected telephone number" MODEM + "ISDN service telephone number" ISDN + "Personal communication services telephone number" PCS + "Preferred use of a telephone number" PREF } +"Format or preference of an email" enum EmailTags{ + "Address associated with a residence" HOME + "Address associated with a place of work" WORK + "Internet addressing type" INTERNET + "Preferred use of an email address when more than one is specified" PREF + "X.400 addressing type" X400 } From 3eea77fe219a3d86ae95dae51841bfa630515dea Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 23 Sep 2022 16:56:57 +0200 Subject: [PATCH 071/117] Prepare domains backend to allow retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current logic for domain deletion first checks that the domain exists, then removes the domain, and then runs the remove_domain hook. But this is a problem, because if any of the hooks fails, then retrying the domain removal won’t work, because it will be stopped at the first check for domain existence. Here we instead mark the domain as "in progress for deletion", which will already disable it, and only finally delete it _after_ all hook handlers have run hopefully successfully. --- big_tests/tests/graphql_domain_SUITE.erl | 2 +- big_tests/tests/service_domain_db_SUITE.erl | 30 +++--- doc/migrations/5.1.0_6.0.0.md | 7 ++ priv/migrations/mssql_5.1.0_6.0.0.sql | 2 + priv/migrations/mysql_5.1.0_6.0.0.sql | 4 + priv/migrations/pgsql_5.1.0_6.0.0.sql | 10 ++ priv/mssql2012.sql | 2 +- priv/mysql.sql | 2 +- priv/pg.sql | 2 +- src/domain/mongoose_domain_api.erl | 32 ++++-- src/domain/mongoose_domain_sql.erl | 100 ++++++++++-------- .../mongoose_graphql_domain_admin_query.erl | 4 +- 12 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 priv/migrations/mssql_5.1.0_6.0.0.sql create mode 100644 priv/migrations/mysql_5.1.0_6.0.0.sql create mode 100644 priv/migrations/pgsql_5.1.0_6.0.0.sql diff --git a/big_tests/tests/graphql_domain_SUITE.erl b/big_tests/tests/graphql_domain_SUITE.erl index 2e29ec739c1..fc777f8427f 100644 --- a/big_tests/tests/graphql_domain_SUITE.erl +++ b/big_tests/tests/graphql_domain_SUITE.erl @@ -145,7 +145,7 @@ disable_domain(Config) -> ParsedResult = get_ok_value([data, domain, disableDomain], Result), ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"enabled">> := false}, ParsedResult), {ok, Domain} = rpc(mim(), mongoose_domain_sql, select_domain, [?EXAMPLE_DOMAIN]), - ?assertEqual(#{host_type => ?HOST_TYPE, enabled => false}, Domain). + ?assertEqual(#{host_type => ?HOST_TYPE, status => disabled}, Domain). enable_domain(Config) -> Result = enable_domain(?EXAMPLE_DOMAIN, Config), diff --git a/big_tests/tests/service_domain_db_SUITE.erl b/big_tests/tests/service_domain_db_SUITE.erl index 54f667e150a..69504891056 100644 --- a/big_tests/tests/service_domain_db_SUITE.erl +++ b/big_tests/tests/service_domain_db_SUITE.erl @@ -370,7 +370,7 @@ db_get_all_dynamic(_) -> db_inserted_domain_is_in_db(_) -> ok = insert_domain(mim(), <<"example.db">>, <<"type1">>), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). db_inserted_domain_is_in_core(_) -> @@ -387,7 +387,7 @@ db_deleted_domain_fails_with_wrong_host_type(_) -> ok = insert_domain(mim(), <<"example.db">>, <<"type1">>), {error, wrong_host_type} = delete_domain(mim(), <<"example.db">>, <<"type2">>), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). db_deleted_domain_from_core(_) -> @@ -400,7 +400,7 @@ db_deleted_domain_from_core(_) -> db_disabled_domain_is_in_db(_) -> ok = insert_domain(mim(), <<"example.db">>, <<"type1">>), ok = disable_domain(mim(), <<"example.db">>), - {ok, #{host_type := <<"type1">>, enabled := false}} = + {ok, #{host_type := <<"type1">>, status := disabled}} = select_domain(mim(), <<"example.db">>). db_disabled_domain_not_in_core(_) -> @@ -413,7 +413,7 @@ db_reenabled_domain_is_in_db(_) -> ok = insert_domain(mim(), <<"example.db">>, <<"type1">>), ok = disable_domain(mim(), <<"example.db">>), ok = enable_domain(mim(), <<"example.db">>), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). db_reenabled_domain_is_in_core(_) -> @@ -528,7 +528,7 @@ db_records_are_restored_on_mim_restart(_) -> {error, not_found} = get_host_type(mim(), <<"example.com">>), service_enabled(mim()), %% DB still contains data - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.com">>), %% Restored {ok, <<"type1">>} = get_host_type(mim(), <<"example.com">>). @@ -542,9 +542,9 @@ db_record_is_ignored_if_domain_static(_) -> restart_domain_core(mim(), [{<<"example.com">>, <<"cfggroup">>}], [<<"dbgroup">>, <<"cfggroup">>]), service_enabled(mim()), %% DB still contains data - {ok, #{host_type := <<"dbgroup">>, enabled := true}} = + {ok, #{host_type := <<"dbgroup">>, status := enabled}} = select_domain(mim(), <<"example.com">>), - {ok, #{host_type := <<"dbgroup">>, enabled := true}} = + {ok, #{host_type := <<"dbgroup">>, status := enabled}} = select_domain(mim(), <<"example.net">>), %% Static DB records are ignored {ok, <<"cfggroup">>} = get_host_type(mim(), <<"example.com">>), @@ -719,20 +719,20 @@ db_event_could_appear_with_lower_id(_Config) -> cli_can_insert_domain(Config) -> {"Added\n", 0} = mongooseimctl("insert_domain", [<<"example.db">>, <<"type1">>], Config), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). cli_can_disable_domain(Config) -> mongooseimctl("insert_domain", [<<"example.db">>, <<"type1">>], Config), mongooseimctl("disable_domain", [<<"example.db">>], Config), - {ok, #{host_type := <<"type1">>, enabled := false}} = + {ok, #{host_type := <<"type1">>, status := disabled}} = select_domain(mim(), <<"example.db">>). cli_can_enable_domain(Config) -> mongooseimctl("insert_domain", [<<"example.db">>, <<"type1">>], Config), mongooseimctl("disable_domain", [<<"example.db">>], Config), mongooseimctl("enable_domain", [<<"example.db">>], Config), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). cli_can_delete_domain(Config) -> @@ -824,13 +824,13 @@ cli_disable_domain_fails_if_service_disabled(Config) -> rest_can_insert_domain(Config) -> {{<<"204">>, _}, _} = rest_put_domain(Config, <<"example.db">>, <<"type1">>), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). rest_can_disable_domain(Config) -> rest_put_domain(Config, <<"example.db">>, <<"type1">>), rest_patch_enabled(Config, <<"example.db">>, false), - {ok, #{host_type := <<"type1">>, enabled := false}} = + {ok, #{host_type := <<"type1">>, status := disabled}} = select_domain(mim(), <<"example.db">>). rest_can_delete_domain(Config) -> @@ -941,13 +941,13 @@ rest_can_enable_domain(Config) -> rest_put_domain(Config, <<"example.db">>, <<"type1">>), rest_patch_enabled(Config, <<"example.db">>, false), rest_patch_enabled(Config, <<"example.db">>, true), - {ok, #{host_type := <<"type1">>, enabled := true}} = + {ok, #{host_type := <<"type1">>, status := enabled}} = select_domain(mim(), <<"example.db">>). rest_can_select_domain(Config) -> rest_put_domain(Config, <<"example.db">>, <<"type1">>), {{<<"200">>, <<"OK">>}, - {[{<<"host_type">>, <<"type1">>}, {<<"enabled">>, true}]}} = + {[ {<<"status">>, <<"enabled">>}, {<<"host_type">>, <<"type1">>} ]}} = rest_select_domain(Config, <<"example.db">>). rest_cannot_select_domain_if_domain_not_found(Config) -> @@ -1143,7 +1143,7 @@ erase_database(Node) -> prepare_test_queries(Node) -> case mongoose_helper:is_rdbms_enabled(domain()) of - true -> rpc(Node, mongoose_domain_sql, prepare_test_queries, [global]); + true -> rpc(Node, mongoose_domain_sql, prepare_test_queries, []); false -> ok end. diff --git a/doc/migrations/5.1.0_6.0.0.md b/doc/migrations/5.1.0_6.0.0.md index c299bfccdb8..3010083757a 100644 --- a/doc/migrations/5.1.0_6.0.0.md +++ b/doc/migrations/5.1.0_6.0.0.md @@ -15,3 +15,10 @@ For some endpoints, the response messages may be slightly different because of t ## CTL For some commands, the response messages may be slightly different because of the unification with other APIs. + +## Dynamic domains + +Removing a domain was a potentially troublesome operation: if the removal was to fail midway through the process, retrials wouldn't be accepted. This is fixed now, by first disabling and marking a domain for removal, then running all the handlers, and only on full success will the domain be removed. So if any failure is notified, the whole operation can be retried again. + +The database requires a migration, as the status of a domain takes now more than the two values a boolean allows, see the migrations for Postgres, MySQL and MSSQL in the [`priv/migrations`](https://github.com/esl/MongooseIM/tree/master/priv/migrations) directory. + diff --git a/priv/migrations/mssql_5.1.0_6.0.0.sql b/priv/migrations/mssql_5.1.0_6.0.0.sql new file mode 100644 index 00000000000..d7bd908bf16 --- /dev/null +++ b/priv/migrations/mssql_5.1.0_6.0.0.sql @@ -0,0 +1,2 @@ +-- DOMAINS +sp_rename 'domains_settings.enabled', 'status', 'COLUMN'; diff --git a/priv/migrations/mysql_5.1.0_6.0.0.sql b/priv/migrations/mysql_5.1.0_6.0.0.sql new file mode 100644 index 00000000000..a50f0f671d4 --- /dev/null +++ b/priv/migrations/mysql_5.1.0_6.0.0.sql @@ -0,0 +1,4 @@ +-- DOMAINS +ALTER TABLE domain_settings ALTER COLUMN enabled DROP DEFAULT; +ALTER TABLE domain_settings CHANGE enabled status TINYINT NOT NULL; +ALTER TABLE domain_settings ALTER COLUMN status SET DEFAULT 1; diff --git a/priv/migrations/pgsql_5.1.0_6.0.0.sql b/priv/migrations/pgsql_5.1.0_6.0.0.sql new file mode 100644 index 00000000000..bbe33fa77df --- /dev/null +++ b/priv/migrations/pgsql_5.1.0_6.0.0.sql @@ -0,0 +1,10 @@ +-- DOMAINS +ALTER TABLE domains_settings ALTER COLUMN enabled DROP DEFAULT; + +ALTER TABLE domains_settings + ALTER COLUMN enabled TYPE SMALLINT USING CASE WHEN enabled THEN 1 ELSE 0 END; + +ALTER TABLE domains_settings + RENAME enabled TO status; + +ALTER TABLE domains_settings ALTER COLUMN status SET DEFAULT 1; diff --git a/priv/mssql2012.sql b/priv/mssql2012.sql index 69974340df1..351939358f7 100644 --- a/priv/mssql2012.sql +++ b/priv/mssql2012.sql @@ -741,7 +741,7 @@ CREATE TABLE domain_settings ( id BIGINT IDENTITY(1,1) PRIMARY KEY, domain VARCHAR(250) NOT NULL, host_type VARCHAR(250) NOT NULL, - enabled SMALLINT NOT NULL DEFAULT 1 + status SMALLINT NOT NULL DEFAULT 1 ); -- A new record is inserted into domain_events, each time diff --git a/priv/mysql.sql b/priv/mysql.sql index 78a428c0b46..700889f8cdb 100644 --- a/priv/mysql.sql +++ b/priv/mysql.sql @@ -533,7 +533,7 @@ CREATE TABLE domain_settings ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, domain VARCHAR(250) NOT NULL, host_type VARCHAR(250) NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT true + status TINYINT NOT NULL DEFAULT 1 ); -- A new record is inserted into domain_events, each time diff --git a/priv/pg.sql b/priv/pg.sql index 335b7d62908..d0525f57c84 100644 --- a/priv/pg.sql +++ b/priv/pg.sql @@ -490,7 +490,7 @@ CREATE TABLE domain_settings ( id BIGSERIAL NOT NULL UNIQUE, domain VARCHAR(250) NOT NULL, host_type VARCHAR(250) NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT true, + status SMALLINT NOT NULL DEFAULT 1, PRIMARY KEY(domain) ); diff --git a/src/domain/mongoose_domain_api.erl b/src/domain/mongoose_domain_api.erl index f595bfa6d82..cc28c738f32 100644 --- a/src/domain/mongoose_domain_api.erl +++ b/src/domain/mongoose_domain_api.erl @@ -66,27 +66,39 @@ insert_domain(Domain, HostType) -> Other end. +-type delete_domain_return() :: + ok | {error, static} | {error, unknown_host_type} | {error, service_disabled} + | {error, {db_error, term()}} | {error, wrong_host_type} | {error, {modules_failed, [module()]}}. + %% Returns ok, if domain not found. %% Domain should be nameprepped using `jid:nameprep'. --spec delete_domain(domain(), host_type()) -> - ok | {error, static} | {error, {db_error, term()}} - | {error, service_disabled} | {error, wrong_host_type} | {error, unknown_host_type}. +-spec delete_domain(domain(), host_type()) -> delete_domain_return(). delete_domain(Domain, HostType) -> case check_domain(Domain, HostType) of ok -> - Res = check_db(mongoose_domain_sql:delete_domain(Domain, HostType)), - case Res of + Res0 = check_db(mongoose_domain_sql:set_domain_for_deletion(Domain, HostType)), + case Res0 of ok -> delete_domain_password(Domain), - mongoose_hooks:remove_domain(HostType, Domain); - _ -> - ok - end, - Res; + do_delete_domain_in_progress(Domain, HostType); + Other -> + Other + end; Other -> Other end. +%% This is ran only in the context of `do_delete_domain', +%% so it can already skip some checks +-spec do_delete_domain_in_progress(domain(), host_type()) -> delete_domain_return(). +do_delete_domain_in_progress(Domain, HostType) -> + case mongoose_hooks:remove_domain(HostType, Domain) of + #{failed := []} -> + check_db(mongoose_domain_sql:delete_domain(Domain, HostType)); + #{failed := Failed} -> + {error, {modules_failed, Failed}} + end. + -spec disable_domain(domain()) -> ok | {error, not_found} | {error, static} | {error, service_disabled} | {error, {db_error, term()}}. diff --git a/src/domain/mongoose_domain_sql.erl b/src/domain/mongoose_domain_sql.erl index 56ba2994398..9ad129779cc 100644 --- a/src/domain/mongoose_domain_sql.erl +++ b/src/domain/mongoose_domain_sql.erl @@ -4,6 +4,7 @@ -export([insert_domain/2, delete_domain/2, + set_domain_for_deletion/2, disable_domain/1, enable_domain/1]). @@ -22,44 +23,45 @@ insert_dummy_event/1]). %% interfaces only for integration tests --export([prepare_test_queries/1, +-export([prepare_test_queries/0, erase_database/1, insert_full_event/2, insert_domain_settings_without_event/2]). --ignore_xref([erase_database/1, prepare_test_queries/1, get_enabled_dynamic/0, +-ignore_xref([erase_database/1, prepare_test_queries/0, get_enabled_dynamic/0, insert_full_event/2, insert_domain_settings_without_event/2]). -import(mongoose_rdbms, [prepare/4, execute_successfully/3]). +-type status() :: enabled | disabled | deleting. -type event_id() :: non_neg_integer(). -type domain() :: binary(). -type row() :: {event_id(), domain(), mongooseim:host_type() | null}. -export_type([row/0]). -start(#{db_pool := Pool}) -> +start(_) -> {LimitSQL, LimitMSSQL} = rdbms_queries:get_db_specific_limits_binaries(), - True = sql_true(Pool), + Enabled = integer_to_binary(status_to_int(enabled)), %% Settings prepare(domain_insert_settings, domain_settings, [domain, host_type], <<"INSERT INTO domain_settings (domain, host_type) " "VALUES (?, ?)">>), - prepare(domain_update_settings_enabled, domain_settings, - [enabled, domain], + prepare(domain_update_settings_status, domain_settings, + [status, domain], <<"UPDATE domain_settings " - "SET enabled = ? " + "SET status = ? " "WHERE domain = ?">>), prepare(domain_delete_settings, domain_settings, [domain], <<"DELETE FROM domain_settings WHERE domain = ?">>), prepare(domain_select, domain_settings, [domain], - <<"SELECT host_type, enabled " + <<"SELECT host_type, status " "FROM domain_settings WHERE domain = ?">>), prepare(domain_select_from, domain_settings, rdbms_queries:add_limit_arg(limit, [id]), <<"SELECT ", LimitMSSQL/binary, " id, domain, host_type " " FROM domain_settings " - " WHERE id > ? AND enabled = ", True/binary, " " + " WHERE id > ? AND status = ", Enabled/binary, " " " ORDER BY id ", LimitSQL/binary>>), %% Events @@ -81,7 +83,7 @@ start(#{db_pool := Pool}) -> " FROM domain_events " " LEFT JOIN domain_settings ON " "(domain_settings.domain = domain_events.domain AND " - "domain_settings.enabled = ", True/binary, ") " + "domain_settings.status = ", Enabled/binary, ") " " WHERE domain_events.id >= ? AND domain_events.id <= ? " " ORDER BY domain_events.id ">>), %% Admins @@ -98,29 +100,23 @@ start(#{db_pool := Pool}) -> " FROM domain_admins WHERE domain = ?">>), ok. -prepare_test_queries(Pool) -> - True = sql_true(Pool), +prepare_test_queries() -> + Enabled = integer_to_binary(status_to_int(enabled)), prepare(domain_erase_admins, domain_admins, [], <<"DELETE FROM domain_admins">>), prepare(domain_erase_settings, domain_settings, [], <<"DELETE FROM domain_settings">>), prepare(domain_erase_events, domain_events, [], <<"DELETE FROM domain_events">>), - prepare(domain_get_enabled_dynamic, domain_settings, [], + prepare(domain_get_status_dynamic, domain_settings, [], <<"SELECT " " domain, host_type " " FROM domain_settings " - " WHERE enabled = ", True/binary, " " + " WHERE status = ", Enabled/binary, " " " ORDER BY id">>), prepare(domain_events_get_all, domain_events, [], <<"SELECT id, domain FROM domain_events ORDER BY id">>). -sql_true(Pool) -> - case mongoose_rdbms:db_engine(Pool) of - pgsql -> <<"true">>; - _ -> <<"1">> - end. - %% ---------------------------------------------------------------------------- %% API insert_domain(Domain, HostType) -> @@ -160,11 +156,25 @@ delete_domain(Domain, HostType) -> end end). +set_domain_for_deletion(Domain, HostType) -> + transaction(fun(Pool) -> + case select_domain(Domain) of + {ok, #{host_type := HT}} when HT =:= HostType -> + {updated, 1} = set_domain_for_deletion_settings(Pool, Domain), + insert_domain_event(Pool, Domain), + ok; + {ok, _} -> + {error, wrong_host_type}; + {error, not_found} -> + ok + end + end). + disable_domain(Domain) -> - set_enabled(Domain, false). + set_status(Domain, disabled). enable_domain(Domain) -> - set_enabled(Domain, true). + set_status(Domain, enabled). select_domain_admin(Domain) -> Pool = get_db_pool(), @@ -219,8 +229,8 @@ select_from(FromId, Limit) -> get_enabled_dynamic() -> Pool = get_db_pool(), - prepare_test_queries(Pool), - {selected, Rows} = execute_successfully(Pool, domain_get_enabled_dynamic, []), + prepare_test_queries(), + {selected, Rows} = execute_successfully(Pool, domain_get_status_dynamic, []), Rows. %% FromId, ToId are included into the result @@ -317,43 +327,47 @@ insert_domain_settings(Pool, Domain, HostType) -> delete_domain_settings(Pool, Domain) -> execute_successfully(Pool, domain_delete_settings, [Domain]). -set_enabled(Domain, Enabled) when is_boolean(Enabled) -> +set_domain_for_deletion_settings(Pool, Domain) -> + ExtStatus = status_to_int(deleting), + execute_successfully(Pool, domain_update_settings_status, [ExtStatus, Domain]). + +-spec set_status(domain(), status()) -> ok | {error, term()}. +set_status(Domain, Status) -> transaction(fun(Pool) -> case select_domain(Domain) of {error, Reason} -> {error, Reason}; - {ok, #{enabled := En, host_type := HostType}} -> + {ok, #{status := CurrentStatus, host_type := HostType}} -> case mongoose_domain_core:is_host_type_allowed(HostType) of false -> {error, unknown_host_type}; - true when Enabled =:= En -> + true when Status =:= CurrentStatus -> ok; true -> - update_domain_enabled(Pool, Domain, Enabled), + update_domain_enabled(Pool, Domain, Status), insert_domain_event(Pool, Domain), ok end end end). -update_domain_enabled(Pool, Domain, Enabled) -> - ExtEnabled = bool_to_ext(Pool, Enabled), - execute_successfully(Pool, domain_update_settings_enabled, [ExtEnabled, Domain]). +update_domain_enabled(Pool, Domain, Status) -> + ExtStatus = status_to_int(Status), + execute_successfully(Pool, domain_update_settings_status, [ExtStatus, Domain]). -%% MySQL needs booleans as integers -bool_to_ext(Pool, Bool) when is_boolean(Bool) -> - case mongoose_rdbms:db_engine(Pool) of - pgsql -> - Bool; - _ -> - bool_to_int(Bool) - end. +row_to_map({HostType, Status}) -> + IntStatus = mongoose_rdbms:result_to_integer(Status), + #{host_type => HostType, status => int_to_status(IntStatus)}. -bool_to_int(true) -> 1; -bool_to_int(false) -> 0. +-spec int_to_status(0..2) -> status(). +int_to_status(0) -> disabled; +int_to_status(1) -> enabled; +int_to_status(2) -> deleting. -row_to_map({HostType, Enabled}) -> - #{host_type => HostType, enabled => mongoose_rdbms:to_bool(Enabled)}. +-spec status_to_int(status()) -> 0..2. +status_to_int(disabled) -> 0; +status_to_int(enabled) -> 1; +status_to_int(deleting) -> 2. get_db_pool() -> mongoose_config:get_opt([services, service_domain_db, db_pool]). diff --git a/src/graphql/admin/mongoose_graphql_domain_admin_query.erl b/src/graphql/admin/mongoose_graphql_domain_admin_query.erl index 933fbecac74..6a828d037ff 100644 --- a/src/graphql/admin/mongoose_graphql_domain_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_domain_admin_query.erl @@ -13,9 +13,9 @@ execute(_Ctx, admin, <<"domainsByHostType">>, #{<<"hostType">> := HostType}) -> {ok, Domains2}; execute(_Ctx, admin, <<"domainDetails">>, #{<<"domain">> := Domain}) -> case mongoose_domain_sql:select_domain(Domain) of - {ok, #{host_type := HostType, enabled := Enabled}} -> + {ok, #{host_type := HostType, status := Status}} -> {ok, #domain{host_type = HostType, domain = Domain, - enabled = Enabled}}; + enabled = Status =:= enabled}}; {error, not_found} -> {error, #{what => domain_not_found, domain => Domain}} end. From b3c25aa872001b5e43075189797ac73f41c998bc Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 23 Sep 2022 16:58:17 +0200 Subject: [PATCH 072/117] Enable to notify of modules failing to remove their data --- src/mongoose_hooks.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 4c876d4aba5..5ed0e0b3bca 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -235,9 +235,9 @@ does_user_exist(HostType, Jid, RequestType) -> -spec remove_domain(HostType, Domain) -> Result when HostType :: binary(), Domain :: jid:lserver(), - Result :: ok. + Result :: #{failed => [module()]}. remove_domain(HostType, Domain) -> - run_hook_for_host_type(remove_domain, HostType, #{}, [HostType, Domain]). + run_hook_for_host_type(remove_domain, HostType, #{failed => []}, [HostType, Domain]). -spec node_cleanup(Node :: node()) -> Acc :: map(). node_cleanup(Node) -> From 471d300574b8eb36edfb06db10bfb1ddc2d003c2 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 26 Sep 2022 15:17:31 +0200 Subject: [PATCH 073/117] Define Status in GraphQL schema --- big_tests/tests/graphql_domain_SUITE.erl | 10 +++++----- priv/graphql/schemas/global/domain.gql | 11 ++++++++++- src/domain/mongoose_domain_api.erl | 3 ++- src/domain/mongoose_domain_sql.erl | 9 ++++----- .../admin/mongoose_graphql_domain_admin_mutation.erl | 4 ++-- .../admin/mongoose_graphql_domain_admin_query.erl | 3 +-- src/graphql/mongoose_graphql_domain.erl | 4 ++-- src/graphql/mongoose_graphql_enum.erl | 4 ++++ src/graphql/mongoose_graphql_types.hrl | 2 +- 9 files changed, 31 insertions(+), 19 deletions(-) diff --git a/big_tests/tests/graphql_domain_SUITE.erl b/big_tests/tests/graphql_domain_SUITE.erl index fc777f8427f..8fa4a3fd0b2 100644 --- a/big_tests/tests/graphql_domain_SUITE.erl +++ b/big_tests/tests/graphql_domain_SUITE.erl @@ -103,7 +103,7 @@ create_domain(DomainName, Config) -> ParsedResult = get_ok_value([data, domain, addDomain], Result), ?assertEqual(#{<<"domain">> => DomainName, <<"hostType">> => ?HOST_TYPE, - <<"enabled">> => null}, ParsedResult). + <<"status">> => null}, ParsedResult). unknown_host_type_error_formatting(Config) -> DomainName = ?EXAMPLE_DOMAIN, @@ -143,14 +143,14 @@ wrong_host_type_error_formatting(Config) -> disable_domain(Config) -> Result = disable_domain(?EXAMPLE_DOMAIN, Config), ParsedResult = get_ok_value([data, domain, disableDomain], Result), - ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"enabled">> := false}, ParsedResult), + ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"status">> := <<"DISABLED">>}, ParsedResult), {ok, Domain} = rpc(mim(), mongoose_domain_sql, select_domain, [?EXAMPLE_DOMAIN]), ?assertEqual(#{host_type => ?HOST_TYPE, status => disabled}, Domain). enable_domain(Config) -> Result = enable_domain(?EXAMPLE_DOMAIN, Config), ParsedResult = get_ok_value([data, domain, enableDomain], Result), - ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"enabled">> := true}, ParsedResult). + ?assertMatch(#{<<"domain">> := ?EXAMPLE_DOMAIN, <<"status">> := <<"ENABLED">>}, ParsedResult). get_domains_by_host_type(Config) -> Result = get_domains_by_host_type(?HOST_TYPE, Config), @@ -163,7 +163,7 @@ get_domain_details(Config) -> ParsedResult = get_ok_value([data, domain, domainDetails], Result), ?assertEqual(#{<<"domain">> => ?EXAMPLE_DOMAIN, <<"hostType">> => ?HOST_TYPE, - <<"enabled">> => true}, ParsedResult). + <<"status">> => <<"ENABLED">>}, ParsedResult). delete_domain(Config) -> Result1 = remove_domain(?EXAMPLE_DOMAIN, ?HOST_TYPE, Config), @@ -202,7 +202,7 @@ domain_admin_get_domain_details(Config) -> ParsedResult = get_ok_value([data, domain, domainDetails], Result), ?assertEqual(#{<<"domain">> => ?DOMAIN_ADMIN_EXAMPLE_DOMAIN, <<"hostType">> => ?HOST_TYPE, - <<"enabled">> => true}, ParsedResult). + <<"status">> => <<"ENABLED">>}, ParsedResult). domain_admin_set_domain_password(Config) -> Result = set_domain_password(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, <<"secret">>, Config), diff --git a/priv/graphql/schemas/global/domain.gql b/priv/graphql/schemas/global/domain.gql index 37fb23994a3..19443fcd2ec 100644 --- a/priv/graphql/schemas/global/domain.gql +++ b/priv/graphql/schemas/global/domain.gql @@ -8,5 +8,14 @@ type Domain{ "Domain hostType" hostType: String "Is domain enabled?" - enabled: Boolean + status: DomainStatus +} + +enum DomainStatus { + "Domain is enabled and ready to route traffic" + ENABLED + "Domain is disabled and won't be loaded into MongooseIM" + DISABLED + "Domain has been marked for deletion and is disabled until all data is removed" + DELETING } diff --git a/src/domain/mongoose_domain_api.erl b/src/domain/mongoose_domain_api.erl index cc28c738f32..d7ee29943e0 100644 --- a/src/domain/mongoose_domain_api.erl +++ b/src/domain/mongoose_domain_api.erl @@ -34,10 +34,11 @@ -ignore_xref([get_all_dynamic/0]). -ignore_xref([stop/0]). +-type status() :: enabled | disabled | deleting. -type domain() :: jid:lserver(). -type host_type() :: mongooseim:host_type(). -type subdomain_pattern() :: mongoose_subdomain_utils:subdomain_pattern(). - +-export_type([status/0]). -spec init() -> ok | {error, term()}. init() -> diff --git a/src/domain/mongoose_domain_sql.erl b/src/domain/mongoose_domain_sql.erl index 9ad129779cc..7844d4a1f11 100644 --- a/src/domain/mongoose_domain_sql.erl +++ b/src/domain/mongoose_domain_sql.erl @@ -33,9 +33,8 @@ -import(mongoose_rdbms, [prepare/4, execute_successfully/3]). --type status() :: enabled | disabled | deleting. -type event_id() :: non_neg_integer(). --type domain() :: binary(). +-type domain() :: jid:lserver(). -type row() :: {event_id(), domain(), mongooseim:host_type() | null}. -export_type([row/0]). @@ -331,7 +330,7 @@ set_domain_for_deletion_settings(Pool, Domain) -> ExtStatus = status_to_int(deleting), execute_successfully(Pool, domain_update_settings_status, [ExtStatus, Domain]). --spec set_status(domain(), status()) -> ok | {error, term()}. +-spec set_status(domain(), mongoose_domain_api:status()) -> ok | {error, term()}. set_status(Domain, Status) -> transaction(fun(Pool) -> case select_domain(Domain) of @@ -359,12 +358,12 @@ row_to_map({HostType, Status}) -> IntStatus = mongoose_rdbms:result_to_integer(Status), #{host_type => HostType, status => int_to_status(IntStatus)}. --spec int_to_status(0..2) -> status(). +-spec int_to_status(0..2) -> mongoose_domain_api:status(). int_to_status(0) -> disabled; int_to_status(1) -> enabled; int_to_status(2) -> deleting. --spec status_to_int(status()) -> 0..2. +-spec status_to_int(mongoose_domain_api:status()) -> 0..2. status_to_int(disabled) -> 0; status_to_int(enabled) -> 1; status_to_int(deleting) -> 2. diff --git a/src/graphql/admin/mongoose_graphql_domain_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_domain_admin_mutation.erl index 311cc7d8b80..1512503ce92 100644 --- a/src/graphql/admin/mongoose_graphql_domain_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_domain_admin_mutation.erl @@ -25,14 +25,14 @@ execute(_Ctx, admin, <<"removeDomain">>, #{<<"domain">> := Domain, <<"hostType"> execute(_Ctx, admin, <<"enableDomain">>, #{<<"domain">> := Domain}) -> case mongoose_domain_api:enable_domain(Domain) of ok -> - {ok, #domain{enabled = true, domain = Domain}}; + {ok, #domain{status = enabled, domain = Domain}}; {error, Error} -> error_handler(Error, Domain, <<>>) end; execute(_Ctx, admin, <<"disableDomain">>, #{<<"domain">> := Domain}) -> case mongoose_domain_api:disable_domain(Domain) of ok -> - {ok, #domain{enabled = false, domain = Domain}}; + {ok, #domain{status = disabled, domain = Domain}}; {error, Error} -> error_handler(Error, Domain, <<>>) end; diff --git a/src/graphql/admin/mongoose_graphql_domain_admin_query.erl b/src/graphql/admin/mongoose_graphql_domain_admin_query.erl index 6a828d037ff..1fca40edfa6 100644 --- a/src/graphql/admin/mongoose_graphql_domain_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_domain_admin_query.erl @@ -14,8 +14,7 @@ execute(_Ctx, admin, <<"domainsByHostType">>, #{<<"hostType">> := HostType}) -> execute(_Ctx, admin, <<"domainDetails">>, #{<<"domain">> := Domain}) -> case mongoose_domain_sql:select_domain(Domain) of {ok, #{host_type := HostType, status := Status}} -> - {ok, #domain{host_type = HostType, domain = Domain, - enabled = Status =:= enabled}}; + {ok, #domain{host_type = HostType, domain = Domain, status = Status}}; {error, not_found} -> {error, #{what => domain_not_found, domain => Domain}} end. diff --git a/src/graphql/mongoose_graphql_domain.erl b/src/graphql/mongoose_graphql_domain.erl index f516fa800ec..fd4e89690fe 100644 --- a/src/graphql/mongoose_graphql_domain.erl +++ b/src/graphql/mongoose_graphql_domain.erl @@ -8,7 +8,7 @@ execute(_Ctx, #domain{host_type = HostType}, <<"hostType">>, _Args) -> {ok, HostType}; -execute(_Ctx, #domain{enabled = Enabled}, <<"enabled">>, _Args) -> - {ok, Enabled}; +execute(_Ctx, #domain{status = Status}, <<"status">>, _Args) -> + {ok, Status}; execute(_Ctx, #domain{domain = Name}, <<"domain">>, _Args) -> {ok, Name}. diff --git a/src/graphql/mongoose_graphql_enum.erl b/src/graphql/mongoose_graphql_enum.erl index 7903e24e1e7..27d4095e42f 100644 --- a/src/graphql/mongoose_graphql_enum.erl +++ b/src/graphql/mongoose_graphql_enum.erl @@ -4,6 +4,8 @@ -ignore_xref([input/2, output/2]). +input(<<"DomainStatus">>, Type) -> + {ok, list_to_binary(string:to_lower(binary_to_list(Type)))}; input(<<"PresenceShow">>, Show) -> {ok, list_to_binary(string:to_lower(binary_to_list(Show)))}; input(<<"PresenceType">>, Type) -> @@ -36,6 +38,8 @@ input(<<"TelephoneTags">>, Name) -> {ok, Name}; input(<<"LogLevel">>, Name) -> {ok, list_to_atom(string:to_lower(binary_to_list(Name)))}; input(<<"MetricType">>, Name) -> {ok, Name}. +output(<<"DomainStatus">>, Type) -> + {ok, list_to_binary(string:to_upper(atom_to_list(Type)))}; output(<<"PresenceShow">>, Show) -> {ok, list_to_binary(string:to_upper(binary_to_list(Show)))}; output(<<"PresenceType">>, Type) -> diff --git a/src/graphql/mongoose_graphql_types.hrl b/src/graphql/mongoose_graphql_types.hrl index dc3de12de56..859cab7c414 100644 --- a/src/graphql/mongoose_graphql_types.hrl +++ b/src/graphql/mongoose_graphql_types.hrl @@ -1,7 +1,7 @@ -record(domain, { domain :: binary(), host_type = null :: binary() | null, - enabled = null :: boolean() | null + status = null :: mongoose_domain_api:status() | null }). -record(resolver_error, {reason :: atom(), From aec0106fc60d2a9254d22582d160b26917e33941 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 23 Sep 2022 19:22:35 +0200 Subject: [PATCH 074/117] Define a wrapper to catch error on deletion --- src/domain/mongoose_domain_api.erl | 21 ++++++++++++++++++++- src/mongoose_hooks.erl | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/domain/mongoose_domain_api.erl b/src/domain/mongoose_domain_api.erl index d7ee29943e0..15c20769080 100644 --- a/src/domain/mongoose_domain_api.erl +++ b/src/domain/mongoose_domain_api.erl @@ -2,6 +2,8 @@ %% management. -module(mongoose_domain_api). +-include("mongoose_logger.hrl"). + -export([init/0, stop/0, get_host_type/1]). @@ -27,6 +29,9 @@ get_subdomain_info/1, get_all_subdomains_for_domain/1]). +%% Helper for remove_domain +-export([remove_domain_wrapper/3]). + %% For testing -export([get_all_dynamic/0]). @@ -38,7 +43,9 @@ -type domain() :: jid:lserver(). -type host_type() :: mongooseim:host_type(). -type subdomain_pattern() :: mongoose_subdomain_utils:subdomain_pattern(). --export_type([status/0]). +-type remove_domain_acc() :: #{failed := [module()]}. + +-export_type([status/0, remove_domain_acc/0]). -spec init() -> ok | {error, term()}. init() -> @@ -251,3 +258,15 @@ unregister_subdomain(HostType, SubdomainPattern) -> [mongoose_subdomain_core:subdomain_info()]. get_all_subdomains_for_domain(Domain) -> mongoose_subdomain_core:get_all_subdomains_for_domain(Domain). + +-spec remove_domain_wrapper(remove_domain_acc(), fun(() -> remove_domain_acc()), module()) -> + remove_domain_acc() | {stop, remove_domain_acc()}. +remove_domain_wrapper(Acc, F, Module) -> + try F() + catch C:R:S -> + ?LOG_ERROR(#{what => hook_failed, + text => <<"Error running hook">>, + module => Module, + class => C, reason => R, stacktrace => S}), + {stop, Acc#{failed := [Module | maps:get(failed, Acc)]}} + end. diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 5ed0e0b3bca..474d6f415a0 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -235,7 +235,7 @@ does_user_exist(HostType, Jid, RequestType) -> -spec remove_domain(HostType, Domain) -> Result when HostType :: binary(), Domain :: jid:lserver(), - Result :: #{failed => [module()]}. + Result :: mongoose_domain_api:remove_domain_acc(). remove_domain(HostType, Domain) -> run_hook_for_host_type(remove_domain, HostType, #{failed => []}, [HostType, Domain]). From abc54990bfc3b8ffe6c9baef994e506e394bc158 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 23 Sep 2022 20:24:34 +0200 Subject: [PATCH 075/117] Fail if mam fails --- src/mam/mod_mam_muc_rdbms_arch.erl | 18 ++++++++++-------- src/mam/mod_mam_rdbms_arch.erl | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/mam/mod_mam_muc_rdbms_arch.erl b/src/mam/mod_mam_muc_rdbms_arch.erl index e08248b9294..24c71d84fe5 100644 --- a/src/mam/mod_mam_muc_rdbms_arch.erl +++ b/src/mam/mod_mam_muc_rdbms_arch.erl @@ -312,15 +312,17 @@ remove_archive(Acc, HostType, ArcID, _ArcJID) -> mongoose_rdbms:execute_successfully(HostType, mam_muc_archive_remove, [ArcID]), Acc. --spec remove_domain(mongoose_hooks:simple_acc(), - mongooseim:host_type(), jid:lserver()) -> - mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_domain_api:remove_domain_acc(), host_type(), jid:lserver()) -> + mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> - SubHosts = get_subhosts(HostType, Domain), - {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> - [remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] - end), - Acc. + F = fun() -> + SubHosts = get_subhosts(HostType, Domain), + {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> + [remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] + end), + Acc + end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). remove_domain_trans(HostType, MucHost) -> mongoose_rdbms:execute_successfully(HostType, mam_muc_remove_domain, [MucHost]), diff --git a/src/mam/mod_mam_rdbms_arch.erl b/src/mam/mod_mam_rdbms_arch.erl index f72d05c8ecd..5ec201ac856 100644 --- a/src/mam/mod_mam_rdbms_arch.erl +++ b/src/mam/mod_mam_rdbms_arch.erl @@ -341,15 +341,18 @@ remove_archive(Acc, HostType, ArcID, _ArcJID) -> mongoose_rdbms:execute_successfully(HostType, mam_archive_remove, [ArcID]), Acc. --spec remove_domain(mongoose_hooks:simple_acc(), host_type(), jid:lserver()) -> - mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_domain_api:remove_domain_acc(), host_type(), jid:lserver()) -> + mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> - {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain, [Domain]), - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_prefs, [Domain]), - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_users, [Domain]) - end), - Acc. + F = fun() -> + {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain, [Domain]), + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_prefs, [Domain]), + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_users, [Domain]) + end), + Acc + end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). %% GDPR logic extract_gdpr_messages(Env, ArcID) -> From 9458f4814f069369f73642b23d00e17f45b54e8c Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 24 Sep 2022 13:45:27 +0200 Subject: [PATCH 076/117] Fail if inbox fails --- src/inbox/mod_inbox.erl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index f73cadc56fa..4acefb44507 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -260,12 +260,14 @@ remove_user(Acc, User, Server) -> mod_inbox_utils:clear_inbox(HostType, User, Server), Acc. --spec remove_domain(mongoose_hooks:simple_acc(), - mongooseim:host_type(), jid:lserver()) -> - mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_domain_api:remove_domain_acc(), mongooseim:host_type(), jid:lserver()) -> + mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> - mod_inbox_backend:remove_domain(HostType, Domain), - Acc. + F = fun() -> + mod_inbox_backend:remove_domain(HostType, Domain), + Acc + end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). -spec disco_local_features(mongoose_disco:feature_acc(), map(), From 7ee654ef447906218df43ad299d0a11ab1e144a4 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 24 Sep 2022 14:23:17 +0200 Subject: [PATCH 077/117] Fail if auth fails --- src/auth/ejabberd_auth.erl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/auth/ejabberd_auth.erl b/src/auth/ejabberd_auth.erl index 6044d1b19e6..8ab4df25dc4 100644 --- a/src/auth/ejabberd_auth.erl +++ b/src/auth/ejabberd_auth.erl @@ -425,12 +425,15 @@ auth_methods(HostType) -> auth_method_to_module(Method) -> list_to_atom("ejabberd_auth_" ++ atom_to_list(Method)). --spec remove_domain(mongoose_hooks:simple_acc(), mongooseim:host_type(), jid:lserver()) -> - mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_domain_api:remove_domain_acc(), mongooseim:host_type(), jid:lserver()) -> + mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> - F = fun(Mod) -> mongoose_gen_auth:remove_domain(Mod, HostType, Domain) end, - call_auth_modules_for_host_type(HostType, F, #{op => map}), - Acc. + F = fun() -> + FAuth = fun(Mod) -> mongoose_gen_auth:remove_domain(Mod, HostType, Domain) end, + call_auth_modules_for_host_type(HostType, FAuth, #{op => map}), + Acc + end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). ensure_metrics(Host) -> Metrics = [authorize, check_password, try_register, does_user_exist], From 2710bd42f018dc0febc59df9f985c9631f7ec9c4 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Sat, 24 Sep 2022 14:23:53 +0200 Subject: [PATCH 078/117] Fail if muclight fails --- src/muc_light/mod_muc_light.erl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/muc_light/mod_muc_light.erl b/src/muc_light/mod_muc_light.erl index 29af66aab78..d1b509193d1 100644 --- a/src/muc_light/mod_muc_light.erl +++ b/src/muc_light/mod_muc_light.erl @@ -420,13 +420,15 @@ remove_user(Acc, User, Server) -> Acc end. --spec remove_domain(mongoose_hooks:simple_acc(), - mongooseim:host_type(), jid:lserver()) -> - mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_domain_api:remove_domain_acc(), mongooseim:host_type(), jid:lserver()) -> + mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> - MUCHost = server_host_to_muc_host(HostType, Domain), - mod_muc_light_db_backend:remove_domain(HostType, MUCHost, Domain), - Acc. + F = fun() -> + MUCHost = server_host_to_muc_host(HostType, Domain), + mod_muc_light_db_backend:remove_domain(HostType, MUCHost, Domain), + Acc + end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). -spec add_rooms_to_roster(Acc :: mongoose_acc:t(), UserJID :: jid:jid()) -> mongoose_acc:t(). add_rooms_to_roster(Acc, UserJID) -> From f04024fa729a596f0e00a38f89e239307fdb6b69 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 26 Sep 2022 16:58:52 +0200 Subject: [PATCH 079/117] Remove whitespace --- big_tests/tests/domain_removal_SUITE.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/big_tests/tests/domain_removal_SUITE.erl b/big_tests/tests/domain_removal_SUITE.erl index dc1a6163f12..681df34f959 100644 --- a/big_tests/tests/domain_removal_SUITE.erl +++ b/big_tests/tests/domain_removal_SUITE.erl @@ -394,18 +394,18 @@ last_removal(Config0) -> PresUn = escalus_client:wait_for_stanza(Alice), escalus:assert(is_presence_with_type, [<<"unavailable">>], PresUn), - + %% Alice asks for Bob's last availability BobShortJID = escalus_client:short_jid(Bob), GetLast = escalus_stanza:last_activity(BobShortJID), Stanza = escalus_client:send_iq_and_wait_for_result(Alice, GetLast), - + %% Alice receives Bob's status and last online time > 0 escalus:assert(is_last_result, Stanza), true = (1 =< get_last_activity(Stanza)), <<"I am a banana!">> = get_last_status(Stanza), - - run_remove_domain(), + + run_remove_domain(), escalus_client:send(Alice, GetLast), Error = escalus_client:wait_for_stanza(Alice), escalus:assert(is_error, [<<"auth">>, <<"forbidden">>], Error) From f8754165ce781a21248d99b10cd6a8ec58171ad8 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Mon, 26 Sep 2022 20:41:59 +0200 Subject: [PATCH 080/117] Test that the domain removal is stopped and can continue on retry --- big_tests/tests/domain_removal_SUITE.erl | 46 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/big_tests/tests/domain_removal_SUITE.erl b/big_tests/tests/domain_removal_SUITE.erl index 681df34f959..a734b052bef 100644 --- a/big_tests/tests/domain_removal_SUITE.erl +++ b/big_tests/tests/domain_removal_SUITE.erl @@ -25,7 +25,8 @@ all() -> {group, offline_removal}, {group, markers_removal}, {group, vcard_removal}, - {group, last_removal} + {group, last_removal}, + {group, removal_failures} ]. groups() -> @@ -43,7 +44,8 @@ groups() -> {offline_removal, [], [offline_removal]}, {markers_removal, [], [markers_removal]}, {vcard_removal, [], [vcard_removal]}, - {last_removal, [], [last_removal]} + {last_removal, [], [last_removal]}, + {removal_failures, [], [removal_stops_if_handler_fails]} ]. %%%=================================================================== @@ -80,6 +82,8 @@ end_per_group(_Groupname, Config) -> end, ok. +group_to_modules(removal_failures) -> + group_to_modules(mam_removal); group_to_modules(auth_removal) -> []; group_to_modules(cache_removal) -> @@ -412,7 +416,45 @@ last_removal(Config0) -> end, escalus:fresh_story_with_config(Config0, [{alice, 1}, {bob, 1}], F). +removal_stops_if_handler_fails(Config0) -> + mongoose_helper:inject_module(?MODULE), + F = fun(Config, Alice) -> + start_domain_removal_hook(), + Room = muc_helper:fresh_room_name(), + MucHost = muc_light_helper:muc_host(), + muc_light_helper:create_room(Room, MucHost, alice, [], Config, muc_light_helper:ver(1)), + RoomAddr = <>, + escalus:send(Alice, escalus_stanza:groupchat_to(RoomAddr, <<"text">>)), + escalus:wait_for_stanza(Alice), + mam_helper:wait_for_room_archive_size(MucHost, Room, 1), + run_remove_domain(), + mam_helper:wait_for_room_archive_size(MucHost, Room, 1), + stop_domain_removal_hook(), + run_remove_domain(), + mam_helper:wait_for_room_archive_size(MucHost, Room, 0) + end, + escalus_fresh:story_with_config(Config0, [{alice, 1}], F). + %% Helpers +start_domain_removal_hook() -> + rpc(mim(), ?MODULE, rpc_start_domain_removal_hook, [host_type()]). + +stop_domain_removal_hook() -> + rpc(mim(), ?MODULE, rpc_stop_domain_removal_hook, [host_type()]). + +rpc_start_domain_removal_hook(HostType) -> + gen_hook:add_handler(remove_domain, HostType, + fun ?MODULE:domain_removal_hook_fn/3, + #{}, 30). %% Priority is so that it comes before muclight and mam + +rpc_stop_domain_removal_hook(HostType) -> + gen_hook:delete_handler(remove_domain, HostType, + fun ?MODULE:domain_removal_hook_fn/3, + #{}, 30). + +domain_removal_hook_fn(Acc, _Params, _Extra) -> + F = fun() -> throw(first_time_needs_to_fail) end, + mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). connect_and_disconnect(Spec) -> {ok, Client, _} = escalus_connection:start(Spec), From d2b76dbb7690f7c8b7998725bc4ef89488f776f4 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Sep 2022 12:52:15 +0200 Subject: [PATCH 081/117] Small refactor cleaup --- src/mam/mod_mam_muc_rdbms_arch.erl | 14 ++------------ src/mam/mod_mam_rdbms_arch.erl | 12 ++---------- src/mam/mod_mam_utils.erl | 16 +++++++++++----- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/mam/mod_mam_muc_rdbms_arch.erl b/src/mam/mod_mam_muc_rdbms_arch.erl index 24c71d84fe5..2bedd026343 100644 --- a/src/mam/mod_mam_muc_rdbms_arch.erl +++ b/src/mam/mod_mam_muc_rdbms_arch.erl @@ -39,8 +39,6 @@ -include("mongoose.hrl"). -include("jlib.hrl"). --include_lib("exml/include/exml.hrl"). --include("mongoose_rsm.hrl"). -include("mongoose_mam.hrl"). %% ---------------------------------------------------------------------- @@ -173,8 +171,8 @@ env_vars(HostType, ArcJID) -> decode_row_fn => fun row_to_uniform_format/2, has_message_retraction => mod_mam_utils:has_message_retraction(mod_mam_muc, HostType), has_full_text_search => mod_mam_utils:has_full_text_search(mod_mam_muc, HostType), - db_jid_codec => db_jid_codec(HostType, ?MODULE), - db_message_codec => db_message_codec(HostType, ?MODULE)}. + db_jid_codec => mod_mam_utils:db_jid_codec(HostType, ?MODULE), + db_message_codec => mod_mam_utils:db_message_codec(HostType, ?MODULE)}. row_to_uniform_format(Row, Env) -> mam_decoder:decode_muc_row(Row, Env). @@ -196,14 +194,6 @@ column_names(Mappings) -> %% ---------------------------------------------------------------------- %% Options --spec db_jid_codec(host_type(), module()) -> module(). -db_jid_codec(HostType, Module) -> - gen_mod:get_module_opt(HostType, Module, db_jid_format). - --spec db_message_codec(host_type(), module()) -> module(). -db_message_codec(HostType, Module) -> - gen_mod:get_module_opt(HostType, Module, db_message_format). - -spec get_retract_id(exml:element(), env_vars()) -> none | mod_mam_utils:retraction_id(). get_retract_id(Packet, #{has_message_retraction := Enabled}) -> mod_mam_utils:get_retract_id(Enabled, Packet). diff --git a/src/mam/mod_mam_rdbms_arch.erl b/src/mam/mod_mam_rdbms_arch.erl index 5ec201ac856..29d2c5bd837 100644 --- a/src/mam/mod_mam_rdbms_arch.erl +++ b/src/mam/mod_mam_rdbms_arch.erl @@ -197,8 +197,8 @@ env_vars(HostType, ArcJID) -> decode_row_fn => fun row_to_uniform_format/2, has_message_retraction => mod_mam_utils:has_message_retraction(mod_mam_pm, HostType), has_full_text_search => mod_mam_utils:has_full_text_search(mod_mam_pm, HostType), - db_jid_codec => db_jid_codec(HostType, ?MODULE), - db_message_codec => db_message_codec(HostType, ?MODULE)}. + db_jid_codec => mod_mam_utils:db_jid_codec(HostType, ?MODULE), + db_message_codec => mod_mam_utils:db_message_codec(HostType, ?MODULE)}. row_to_uniform_format(Row, Env) -> mam_decoder:decode_row(Row, Env). @@ -226,14 +226,6 @@ column_names(Mappings) -> %% ---------------------------------------------------------------------- %% Options --spec db_jid_codec(host_type(), module()) -> module(). -db_jid_codec(HostType, Module) -> - gen_mod:get_module_opt(HostType, Module, db_jid_format). - --spec db_message_codec(host_type(), module()) -> module(). -db_message_codec(HostType, Module) -> - gen_mod:get_module_opt(HostType, Module, db_message_format). - -spec get_retract_id(exml:element(), env_vars()) -> none | mod_mam_utils:retraction_id(). get_retract_id(Packet, #{has_message_retraction := Enabled}) -> mod_mam_utils:get_retract_id(Enabled, Packet). diff --git a/src/mam/mod_mam_utils.erl b/src/mam/mod_mam_utils.erl index 83222a1f81e..ecabdbb30b4 100644 --- a/src/mam/mod_mam_utils.erl +++ b/src/mam/mod_mam_utils.erl @@ -90,7 +90,8 @@ %% Shared logic -export([check_result_for_policy_violation/2, - lookup/3]). + lookup/3, + db_message_codec/2, db_jid_codec/2]). -callback extra_fin_element(mongooseim:host_type(), mam_iq:lookup_params(), @@ -1159,7 +1160,7 @@ check_for_item_not_found(#rsm_in{direction = before, id = ID}, _PageSize, {TotalCount, Offset, MessageRows}) -> case maybe_last(MessageRows) of {ok, #{id := ID}} -> - {ok, {TotalCount, Offset, list_without_last(MessageRows)}}; + {ok, {TotalCount, Offset, lists:droplast(MessageRows)}}; undefined -> {error, item_not_found} end; @@ -1221,10 +1222,15 @@ set_complete_result_page_using_extra_message(PageSize, Params, Result = #{messag remove_extra_message(Params, Messages) -> case maps:get(ordering_direction, Params, forward) of forward -> - list_without_last(Messages); + lists:droplast(Messages); backward -> tl(Messages) end. -list_without_last(List) -> - lists:reverse(tl(lists:reverse(List))). +-spec db_jid_codec(mongooseim:host_type(), module()) -> module(). +db_jid_codec(HostType, Module) -> + gen_mod:get_module_opt(HostType, Module, db_jid_format). + +-spec db_message_codec(mongooseim:host_type(), module()) -> module(). +db_message_codec(HostType, Module) -> + gen_mod:get_module_opt(HostType, Module, db_message_format). From 64afba58bd49b739e1daee299ecf6e5c6b891a92 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Sep 2022 13:17:09 +0200 Subject: [PATCH 082/117] Add configuration flag for mam domain_delete operations --- doc/modules/mod_mam.md | 8 ++++++++ src/mam/mod_mam.erl | 6 ++++-- test/common/config_parser_helper.erl | 4 ++-- test/config_parser_SUITE.erl | 3 +++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/doc/modules/mod_mam.md b/doc/modules/mod_mam.md index 8ff5bd5c640..4d1a7b6cf90 100644 --- a/doc/modules/mod_mam.md +++ b/doc/modules/mod_mam.md @@ -360,6 +360,14 @@ This sets the maximum page size of returned results. This enforces all mam lookups to be "simple", i.e., they skip the RSM count. See [Message Archive Management extensions](../open-extensions/mam.md). +#### `modules.mod_mam.delete_domain_limit` + +* **Syntax:** non-negative integer or the string `"infinity"` +* **Default:** `"infinity"` +* **Example:** `modules.mod_mam.delete_domain_limit = 10000` + +Domain deletion can be an expensive operation, as it requires to delete potentially many thousands of records from the DB. By default, the delete operation deletes everything in a transaction, but it might be desired, to handle timeouts and table locks more gracefully, to delete the records in batches. This limit establishes the size of the batch. + #### `modules.mod_mam.db_jid_format` * **Syntax:** string, one of `"mam_jid_rfc"`, `"mam_jid_rfc_trust"`, `"mam_jid_mini"` or a module implementing `mam_jid` behaviour diff --git a/src/mam/mod_mam.erl b/src/mam/mod_mam.erl index ac1818c5af2..8602bd90be7 100644 --- a/src/mam/mod_mam.erl +++ b/src/mam/mod_mam.erl @@ -168,6 +168,8 @@ common_config_items() -> <<"default_result_limit">> => #option{type = integer, validate = non_negative}, <<"enforce_simple_queries">> => #option{type = boolean}, + <<"delete_domain_limit">> => #option{type = int_or_infinity, + validate = positive}, <<"max_result_limit">> => #option{type = integer, validate = non_negative}, <<"db_jid_format">> => #option{type = atom, @@ -296,7 +298,7 @@ parse_backend_opts(elasticsearch, Type, _Opts, Deps0) -> -spec add_rdbms_deps(basic | user_cache | async_writer, mam_type(), module_opts(), module_map()) -> module_map(). add_rdbms_deps(basic, Type, Opts, Deps) -> - Opts1 = maps:with([db_message_format, db_jid_format], Opts), + Opts1 = maps:with([db_message_format, db_jid_format, delete_domain_limit], Opts), Deps1 = add_dep(rdbms_arch_module(Type), maps:merge(rdbms_arch_defaults(Type), Opts1), Deps), add_dep(mod_mam_rdbms_user, user_db_types(Type), Deps1); add_rdbms_deps(user_cache, Type, #{cache_users := true, cache := CacheOpts}, Deps) -> @@ -328,7 +330,7 @@ rdbms_arch_defaults(muc) -> rdbms_arch_defaults() -> #{db_message_format => mam_message_compressed_eterm, - no_writer => false}. + no_writer => false, delete_domain_limit => infinity}. rdbms_arch_module(pm) -> mod_mam_rdbms_arch; rdbms_arch_module(muc) -> mod_mam_muc_rdbms_arch. diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index f579e8f21c5..f9dfa8fd391 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -910,11 +910,11 @@ default_mod_config(mod_mam) -> default_mod_config(mod_mam_muc) -> maps:merge(common_mam_config(), default_config([modules, mod_mam, muc])); default_mod_config(mod_mam_rdbms_arch) -> - #{no_writer => false, + #{no_writer => false, delete_domain_limit => infinity, db_message_format => mam_message_compressed_eterm, db_jid_format => mam_jid_mini}; default_mod_config(mod_mam_muc_rdbms_arch) -> - #{no_writer => false, + #{no_writer => false, delete_domain_limit => infinity, db_message_format => mam_message_compressed_eterm, db_jid_format => mam_jid_rfc}; default_mod_config(mod_muc) -> diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 4893786a73b..1a78cb740a6 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -2045,6 +2045,8 @@ test_mod_mam(P, T) -> T(#{<<"full_text_search">> => false})), ?cfgh(P ++ [cache_users], false, T(#{<<"cache_users">> => false})), + ?cfgh(P ++ [delete_domain_limit], 1000, + T(#{<<"delete_domain_limit">> => 1000})), ?cfgh(P ++ [default_result_limit], 100, T(#{<<"default_result_limit">> => 100})), ?cfgh(P ++ [max_result_limit], 1000, @@ -2067,6 +2069,7 @@ test_mod_mam(P, T) -> ?errh(T(#{<<"user_prefs_store">> => <<"textfile">>})), ?errh(T(#{<<"full_text_search">> => <<"disabled">>})), ?errh(T(#{<<"cache_users">> => []})), + ?errh(T(#{<<"delete_domain_limit">> => []})), ?errh(T(#{<<"default_result_limit">> => -1})), ?errh(T(#{<<"max_result_limit">> => -2})), ?errh(T(#{<<"enforce_simple_queries">> => -2})), From e9eca5b6614111814e0c9db119ca74aa8f980baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Tue, 27 Sep 2022 09:17:07 +0200 Subject: [PATCH 083/117] Make Admin API handlers configurable Also: delegate route specification to handlers. Reason: paths are handler-specific, and it is good to have the whole logic of each handler in its respective module. --- src/mongoose_admin_api/mongoose_admin_api.erl | 54 +++++++++---------- .../mongoose_admin_api_contacts.erl | 16 ++++-- .../mongoose_admin_api_domain.erl | 15 ++++-- .../mongoose_admin_api_inbox.erl | 16 ++++-- .../mongoose_admin_api_messages.erl | 16 ++++-- .../mongoose_admin_api_metrics.erl | 18 +++++-- .../mongoose_admin_api_muc.erl | 16 ++++-- .../mongoose_admin_api_muc_light.erl | 18 +++++-- .../mongoose_admin_api_sessions.erl | 15 ++++-- .../mongoose_admin_api_stanzas.erl | 15 ++++-- .../mongoose_admin_api_users.erl | 15 ++++-- 11 files changed, 144 insertions(+), 70 deletions(-) diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index 53df70d857b..4a0f61ed073 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -24,15 +24,24 @@ -type handler_options() :: #{path := string(), username => binary(), password => binary(), atom() => any()}. -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: #{atom() => any()}. -type error_type() :: bad_request | denied | not_found | duplicate | internal. +-export_type([state/0]). + +-callback routes(state()) -> mongoose_http_handler:routes(). + %% mongoose_http_handler callbacks -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> + Handlers = all_handlers(), #section{items = #{<<"username">> => #option{type = binary}, - <<"password">> => #option{type = binary}}, + <<"password">> => #option{type = binary}, + <<"handlers">> => #list{items = #option{type = atom, + validate = {enum, Handlers}}, + validate = unique}}, + defaults = #{<<"handlers">> => Handlers}, process = fun ?MODULE:process_config/1}. -spec process_config(handler_options()) -> handler_options(). @@ -46,34 +55,19 @@ process_config(Opts) -> -spec routes(handler_options()) -> mongoose_http_handler:routes(). routes(Opts = #{path := BasePath}) -> - [{[BasePath, Path], Module, ModuleOpts} - || {Path, Module, ModuleOpts} <- api_paths(Opts)]. - -api_paths(Opts) -> - [{"/contacts/:user/[:contact]", mongoose_admin_api_contacts, Opts}, - {"/contacts/:user/:contact/manage", mongoose_admin_api_contacts, Opts#{suffix => manage}}, - {"/users/:domain/[:username]", mongoose_admin_api_users, Opts}, - {"/sessions/:domain/[:username]/[:resource]", mongoose_admin_api_sessions, Opts}, - {"/messages/:owner/:with", mongoose_admin_api_messages, Opts}, - {"/messages/[:owner]", mongoose_admin_api_messages, Opts}, - {"/stanzas", mongoose_admin_api_stanzas, Opts}, - {"/muc-lights/:domain", mongoose_admin_api_muc_light, Opts}, - {"/muc-lights/:domain/:id/participants", mongoose_admin_api_muc_light, - Opts#{suffix => participants}}, - {"/muc-lights/:domain/:id/messages", mongoose_admin_api_muc_light, - Opts#{suffix => messages}}, - {"/muc-lights/:domain/:id/management", mongoose_admin_api_muc_light, - Opts#{suffix => management}}, - {"/mucs/:domain", mongoose_admin_api_muc, Opts}, - {"/mucs/:domain/:name/:arg", mongoose_admin_api_muc, Opts}, - {"/inbox/:host_type/:days/bin", mongoose_admin_api_inbox, Opts}, - {"/inbox/:domain/:user/:days/bin", mongoose_admin_api_inbox, Opts}, - {"/domains/:domain", mongoose_admin_api_domain, Opts}, - {"/metrics/", mongoose_admin_api_metrics, Opts}, - {"/metrics/all/[:metric]", mongoose_admin_api_metrics, Opts#{suffix => all}}, - {"/metrics/global/[:metric]", mongoose_admin_api_metrics, Opts#{suffix => global}}, - {"/metrics/host_type/:host_type/[:metric]", mongoose_admin_api_metrics, Opts} - ]. + [{[BasePath, Path], Module, ModuleOpts} || {Path, Module, ModuleOpts} <- api_paths(Opts)]. + +all_handlers() -> + [contacts, users, sessions, messages, stanzas, muc_light, muc, inbox, domain, metrics]. + +-spec api_paths(handler_options()) -> mongoose_http_handler:routes(). +api_paths(#{handlers := Handlers} = Opts) -> + State = maps:with([username, password], Opts), + lists:flatmap(fun(Handler) -> api_paths_for_handler(Handler, State) end, Handlers). + +api_paths_for_handler(Handler, State) -> + HandlerModule = list_to_existing_atom("mongoose_admin_api_" ++ atom_to_list(Handler)), + HandlerModule:routes(State). %% Utilities for the handler modules diff --git a/src/mongoose_admin_api/mongoose_admin_api_contacts.erl b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl index 8d8a05f6dd3..f46d667c626 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_contacts.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_contacts). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -15,11 +18,16 @@ -import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/contacts/:user/[:contact]", ?MODULE, State}, + {"/contacts/:user/:contact/manage", ?MODULE, State#{suffix => manage}}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_domain.erl b/src/mongoose_admin_api/mongoose_admin_api_domain.erl index 907f3243f73..affede9ca67 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_domain.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_domain.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_domain). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -15,11 +18,15 @@ -import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/domains/:domain", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_inbox.erl b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl index 6a9d52d6496..93fdebe863e 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_inbox.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_inbox). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, allowed_methods/2, @@ -9,11 +12,16 @@ -import(mongoose_admin_api, [try_handle_request/3, throw_error/2, respond/3]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/inbox/:host_type/:days/bin", ?MODULE, State}, + {"/inbox/:domain/:user/:days/bin", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_messages.erl b/src/mongoose_admin_api/mongoose_admin_api_messages.erl index 8bf50fae13e..3455f78cd96 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_messages.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_messages.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_messages). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -14,11 +17,16 @@ -import(mongoose_admin_api, [parse_body/1, parse_qs/1, try_handle_request/3, throw_error/2]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/messages/:owner/:with", ?MODULE, State}, + {"/messages/[:owner]", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_metrics.erl b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl index c227885fd14..544c5be00d0 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_metrics.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_metrics). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -12,13 +15,20 @@ -import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). -include("mongoose.hrl"). +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/metrics/", ?MODULE, State}, + {"/metrics/all/[:metric]", ?MODULE, State#{suffix => all}}, + {"/metrics/global/[:metric]", ?MODULE, State#{suffix => global}}, + {"/metrics/host_type/:host_type/[:metric]", ?MODULE, State}]. + -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_muc.erl b/src/mongoose_admin_api/mongoose_admin_api_muc.erl index a84741aafc3..6c8c384bee0 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_muc.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_muc.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_muc). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_accepted/2, @@ -15,11 +18,16 @@ -include("jlib.hrl"). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/mucs/:domain", ?MODULE, State}, + {"/mucs/:domain/:name/:arg", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl b/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl index d68599275fd..81e71cf23fb 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_muc_light). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_accepted/2, @@ -13,11 +16,18 @@ -import(mongoose_admin_api, [try_handle_request/3, throw_error/2, parse_body/1, resource_created/4]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/muc-lights/:domain", ?MODULE, State}, + {"/muc-lights/:domain/:id/participants", ?MODULE, State#{suffix => participants}}, + {"/muc-lights/:domain/:id/messages", ?MODULE, State#{suffix => messages}}, + {"/muc-lights/:domain/:id/management", ?MODULE, State#{suffix => management}}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_sessions.erl b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl index d3e067eda27..2da78694ebe 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_sessions.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_sessions). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -13,11 +16,15 @@ -import(mongoose_admin_api, [try_handle_request/3, throw_error/2]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/sessions/:domain/[:username]/[:resource]", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl b/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl index 7e8af0b60f7..d892f0da8a8 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_stanzas). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_accepted/2, @@ -12,11 +15,15 @@ -import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/stanzas", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> diff --git a/src/mongoose_admin_api/mongoose_admin_api_users.erl b/src/mongoose_admin_api/mongoose_admin_api_users.erl index eb0f210e91e..05db920649f 100644 --- a/src/mongoose_admin_api/mongoose_admin_api_users.erl +++ b/src/mongoose_admin_api/mongoose_admin_api_users.erl @@ -1,6 +1,9 @@ -module(mongoose_admin_api_users). --behaviour(cowboy_rest). +-behaviour(mongoose_admin_api). + -export([routes/1]). + +-behaviour(cowboy_rest). -export([init/2, is_authorized/2, content_types_provided/2, @@ -15,11 +18,15 @@ -import(mongoose_admin_api, [parse_body/1, try_handle_request/3, throw_error/2, resource_created/4]). -type req() :: cowboy_req:req(). --type state() :: map(). +-type state() :: mongoose_admin_api:state(). + +-spec routes(state()) -> mongoose_http_handler:routes(). +routes(State) -> + [{"/users/:domain/[:username]", ?MODULE, State}]. -spec init(req(), state()) -> {cowboy_rest, req(), state()}. -init(Req, Opts) -> - mongoose_admin_api:init(Req, Opts). +init(Req, State) -> + mongoose_admin_api:init(Req, State). -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> From 225a57919bc40f34c25c93d8690eb8a8a21c9e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Tue, 27 Sep 2022 09:18:57 +0200 Subject: [PATCH 084/117] Simplify Client API handler config Also: delegate route specification to handlers. Reason: paths are handler-specific, and it is good to have the whole logic of each handler in its respective module. --- .../mongoose_client_api.erl | 29 ++++++++++--------- .../mongoose_client_api_contacts.erl | 9 +++++- .../mongoose_client_api_messages.erl | 10 +++++-- .../mongoose_client_api_rooms.erl | 11 +++++-- .../mongoose_client_api_rooms_config.erl | 13 +++++---- .../mongoose_client_api_rooms_messages.erl | 11 +++++-- .../mongoose_client_api_rooms_users.erl | 12 ++++---- .../mongoose_client_api_sse.erl | 13 +++++++-- 8 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/mongoose_client_api/mongoose_client_api.erl b/src/mongoose_client_api/mongoose_client_api.erl index 53750134f2e..29bd0fd4261 100644 --- a/src/mongoose_client_api/mongoose_client_api.erl +++ b/src/mongoose_client_api/mongoose_client_api.erl @@ -26,16 +26,18 @@ -type handler_options() :: #{path := string(), handlers := [module()], docs := boolean(), atom() => any()}. +-callback routes() -> mongoose_http_handler:routes(). + %% mongoose_http_handler callbacks -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> - HandlerModules = [Module || {_, Module, _} <- api_paths()], + Handlers = all_handlers(), #section{items = #{<<"handlers">> => #list{items = #option{type = atom, - validate = {enum, HandlerModules}}, + validate = {enum, Handlers}}, validate = unique}, <<"docs">> => #option{type = boolean}}, - defaults = #{<<"handlers">> => HandlerModules, + defaults = #{<<"handlers">> => Handlers, <<"docs">> => true}}. -spec routes(handler_options()) -> mongoose_http_handler:routes(). @@ -43,17 +45,16 @@ routes(Opts = #{path := BasePath}) -> [{[BasePath, Path], Module, ModuleOpts} || {Path, Module, ModuleOpts} <- api_paths(Opts)] ++ api_doc_paths(Opts). -api_paths(#{handlers := HandlerModules}) -> - lists:filter(fun({_, Module, _}) -> lists:member(Module, HandlerModules) end, api_paths()). - -api_paths() -> - [{"/sse", lasse_handler, #{module => mongoose_client_api_sse}}, - {"/messages/[:with]", mongoose_client_api_messages, #{}}, - {"/contacts/[:jid]", mongoose_client_api_contacts, #{}}, - {"/rooms/[:id]", mongoose_client_api_rooms, #{}}, - {"/rooms/[:id]/config", mongoose_client_api_rooms_config, #{}}, - {"/rooms/:id/users/[:user]", mongoose_client_api_rooms_users, #{}}, - {"/rooms/[:id]/messages", mongoose_client_api_rooms_messages, #{}}]. +all_handlers() -> + [sse, messages, contacts, rooms, rooms_config, rooms_users, rooms_messages]. + +-spec api_paths(handler_options()) -> mongoose_http_handler:routes(). +api_paths(#{handlers := Handlers}) -> + lists:flatmap(fun api_paths_for_handler/1, Handlers). + +api_paths_for_handler(Handler) -> + HandlerModule = list_to_existing_atom("mongoose_client_api_" ++ atom_to_list(Handler)), + HandlerModule:routes(). api_doc_paths(#{docs := true}) -> [{"/api-docs", cowboy_swagger_redirect_handler, #{}}, diff --git a/src/mongoose_client_api/mongoose_client_api_contacts.erl b/src/mongoose_client_api/mongoose_client_api_contacts.erl index 2d13aea9719..35755d8198b 100644 --- a/src/mongoose_client_api/mongoose_client_api_contacts.erl +++ b/src/mongoose_client_api/mongoose_client_api_contacts.erl @@ -1,6 +1,9 @@ -module(mongoose_client_api_contacts). --behaviour(cowboy_rest). +-behaviour(mongoose_client_api). +-export([routes/0]). + +-behaviour(cowboy_rest). -export([trails/0]). -export([init/2]). -export([content_types_provided/2]). @@ -19,6 +22,10 @@ -type req() :: cowboy_req:req(). -type state() :: map(). +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/contacts/[:jid]", ?MODULE, #{}}]. + trails() -> mongoose_client_api_contacts_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_messages.erl b/src/mongoose_client_api/mongoose_client_api_messages.erl index 493a68922a9..6792e88fd2d 100644 --- a/src/mongoose_client_api/mongoose_client_api_messages.erl +++ b/src/mongoose_client_api/mongoose_client_api_messages.erl @@ -1,8 +1,10 @@ -module(mongoose_client_api_messages). --behaviour(cowboy_rest). --export([trails/0]). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(cowboy_rest). +-export([trails/0]). -export([init/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). @@ -25,6 +27,10 @@ -include("mongoose_rsm.hrl"). -include_lib("exml/include/exml.hrl"). +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/messages/[:with]", ?MODULE, #{}}]. + trails() -> mongoose_client_api_messages_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms.erl b/src/mongoose_client_api/mongoose_client_api_rooms.erl index fced7ea341e..fa4773fbfd6 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms.erl @@ -1,8 +1,10 @@ -module(mongoose_client_api_rooms). --behaviour(cowboy_rest). --export([trails/0]). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(cowboy_rest). +-export([trails/0]). -export([init/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). @@ -20,7 +22,10 @@ -include("mongoose.hrl"). -include("jlib.hrl"). --include_lib("exml/include/exml.hrl"). + +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/rooms/[:id]", ?MODULE, #{}}]. trails() -> mongoose_client_api_rooms_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_config.erl b/src/mongoose_client_api/mongoose_client_api_rooms_config.erl index 5a062876c2f..da76ac1a3df 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_config.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_config.erl @@ -1,8 +1,10 @@ -module(mongoose_client_api_rooms_config). --behaviour(cowboy_rest). --export([trails/0]). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(cowboy_rest). +-export([trails/0]). -export([init/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). @@ -14,10 +16,9 @@ -ignore_xref([from_json/2, trails/0]). --include("mongoose.hrl"). --include("jlib.hrl"). --include("mongoose_rsm.hrl"). --include_lib("exml/include/exml.hrl"). +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/rooms/[:id]/config", ?MODULE, #{}}]. trails() -> mongoose_client_api_rooms_config_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl b/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl index 7a9c3e10516..0921c768ca4 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl @@ -1,8 +1,10 @@ -module(mongoose_client_api_rooms_messages). --behaviour(cowboy_rest). --export([trails/0]). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(cowboy_rest). +-export([trails/0]). -export([init/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). @@ -21,9 +23,12 @@ -include("mongoose.hrl"). -include("jlib.hrl"). --include("mongoose_rsm.hrl"). -include_lib("exml/include/exml.hrl"). +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/rooms/[:id]/messages", ?MODULE, #{}}]. + trails() -> mongoose_client_api_rooms_messages_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_users.erl b/src/mongoose_client_api/mongoose_client_api_rooms_users.erl index 4139a90ec67..528e58040cd 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_users.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_users.erl @@ -1,8 +1,10 @@ -module(mongoose_client_api_rooms_users). --behaviour(cowboy_rest). --export([trails/0]). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(cowboy_rest). +-export([trails/0]). -export([init/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). @@ -16,9 +18,9 @@ -ignore_xref([from_json/2, trails/0]). --include("mongoose.hrl"). --include("jlib.hrl"). --include_lib("exml/include/exml.hrl"). +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/rooms/:id/users/[:user]", ?MODULE, #{}}]. trails() -> mongoose_client_api_rooms_users_doc:trails(). diff --git a/src/mongoose_client_api/mongoose_client_api_sse.erl b/src/mongoose_client_api/mongoose_client_api_sse.erl index 6c34664e76a..18b3e88df4e 100644 --- a/src/mongoose_client_api/mongoose_client_api_sse.erl +++ b/src/mongoose_client_api/mongoose_client_api_sse.erl @@ -1,15 +1,22 @@ -module(mongoose_client_api_sse). --behaviour(lasse_handler). --include("mongoose.hrl"). --include("jlib.hrl"). +-behaviour(mongoose_client_api). +-export([routes/0]). +-behaviour(lasse_handler). -export([init/3]). -export([handle_notify/2]). -export([handle_info/2]). -export([handle_error/3]). -export([terminate/3]). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/sse", lasse_handler, #{module => mongoose_client_api_sse}}]. + init(_InitArgs, _LastEvtId, Req) -> ?LOG_DEBUG(#{what => client_api_sse_init, req => Req}), {cowboy_rest, Req1, State0} = mongoose_client_api:init(Req, []), From 5ed20d819b064212b96c00327d3ef782ad945105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Tue, 27 Sep 2022 10:13:30 +0200 Subject: [PATCH 085/117] Relax the spec - there is no need for returning all opts --- src/mongoose_http_handler.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index f318e2f84a6..ce8a3bb8a16 100644 --- a/src/mongoose_http_handler.erl +++ b/src/mongoose_http_handler.erl @@ -10,7 +10,7 @@ atom() => any()}. -type path() :: iodata(). --type routes() :: [{path(), module(), options()}]. +-type routes() :: [{path(), module(), #{atom() => any()}}]. -export_type([options/0, path/0, routes/0]). From 4ecb2292754cf7c99104fcf7b93c42d4d4706086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Tue, 27 Sep 2022 09:19:42 +0200 Subject: [PATCH 086/117] Update tests with the new API handler config --- test/common/config_parser_helper.erl | 9 +++++---- test/config_parser_SUITE.erl | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 8e0558d25e8..45c585b444d 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -1074,11 +1074,12 @@ default_config([listen, http, handlers, mod_websockets]) -> #{timeout => 60000, max_stanza_size => infinity, module => mod_websockets}; +default_config([listen, http, handlers, mongoose_admin_api]) -> + #{handlers => [contacts, users, sessions, messages, stanzas, muc_light, muc, + inbox, domain, metrics], + module => mongoose_admin_api}; default_config([listen, http, handlers, mongoose_client_api]) -> - #{handlers => [lasse_handler, mongoose_client_api_messages, - mongoose_client_api_contacts, mongoose_client_api_rooms, - mongoose_client_api_rooms_config, mongoose_client_api_rooms_users, - mongoose_client_api_rooms_messages], + #{handlers => [sse, messages, contacts, rooms, rooms_config, rooms_users, rooms_messages], docs => true, module => mongoose_client_api}; default_config([listen, http, handlers, mongoose_graphql_cowboy_handler]) -> diff --git a/test/config_parser_SUITE.erl b/test/config_parser_SUITE.erl index 51f1ba5ba75..fc002f3af4b 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -595,14 +595,17 @@ listen_http_handlers_websockets(_Config) -> listen_http_handlers_client_api(_Config) -> {P, T} = test_listen_http_handler(mongoose_client_api), - ?cfg(P ++ [handlers], [mongoose_client_api_messages], - T(#{<<"handlers">> => [<<"mongoose_client_api_messages">>]})), + ?cfg(P ++ [handlers], [messages], + T(#{<<"handlers">> => [<<"messages">>]})), ?cfg(P ++ [docs], false, T(#{<<"docs">> => false})), - ?err(T(#{<<"handlers">> => [not_a_module]})), + ?err(T(#{<<"handlers">> => [<<"invalid">>]})), ?err(T(#{<<"docs">> => <<"maybe">>})). listen_http_handlers_admin_api(_Config) -> {P, T} = test_listen_http_handler(mongoose_admin_api), + ?cfg(P ++ [handlers], [muc, inbox], + T(#{<<"handlers">> => [<<"muc">>, <<"inbox">>]})), + ?err(T(#{<<"handlers">> => [<<"invalid">>]})), test_listen_http_handler_creds(P, T). listen_http_handlers_graphql(_Config) -> From 9501f100851dff82d94a9757f05848c1126f363d Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 28 Sep 2022 00:50:12 +0200 Subject: [PATCH 087/117] Implement a configurable incremental delete for mam --- src/mam/mod_mam_muc_rdbms_arch.erl | 46 ++++++++++++++++++++++------- src/mam/mod_mam_rdbms_arch.erl | 47 +++++++++++++++++++++--------- src/mam/mod_mam_utils.erl | 29 ++++++++++++++++++ 3 files changed, 99 insertions(+), 23 deletions(-) diff --git a/src/mam/mod_mam_muc_rdbms_arch.erl b/src/mam/mod_mam_muc_rdbms_arch.erl index 2bedd026343..bc04269d4d9 100644 --- a/src/mam/mod_mam_muc_rdbms_arch.erl +++ b/src/mam/mod_mam_muc_rdbms_arch.erl @@ -52,9 +52,9 @@ %% Starting and stopping functions for users' archives -spec start(host_type(), gen_mod:module_opts()) -> ok. -start(HostType, _Opts) -> +start(HostType, Opts) -> start_hooks(HostType), - register_prepared_queries(), + register_prepared_queries(Opts), ok. -spec stop(host_type()) -> ok. @@ -106,16 +106,25 @@ hooks(HostType) -> %% ---------------------------------------------------------------------- %% SQL queries -register_prepared_queries() -> +register_prepared_queries(Opts) -> prepare_insert(insert_mam_muc_message, 1), mongoose_rdbms:prepare(mam_muc_archive_remove, mam_muc_message, [room_id], <<"DELETE FROM mam_muc_message " "WHERE room_id = ?">>), + + %% Domain Removal + {MaybeLimitSQL, MaybeLimitMSSQL} = mod_mam_utils:batch_delete_limits(Opts), + IdTable = <<"(SELECT ", MaybeLimitMSSQL/binary, + " id from mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, + ServerTable = <<"(SELECT", MaybeLimitMSSQL/binary, + " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, mongoose_rdbms:prepare(mam_muc_remove_domain, mam_muc_message, ['mam_server_user.server'], <<"DELETE FROM mam_muc_message " - "WHERE room_id IN (SELECT id FROM mam_server_user where server = ?)">>), + "WHERE room_id IN ", IdTable/binary>>), mongoose_rdbms:prepare(mam_muc_remove_domain_users, mam_server_user, [server], - <<"DELETE FROM mam_server_user WHERE server = ?">>), + <<"DELETE ", MaybeLimitMSSQL/binary, + " FROM mam_server_user WHERE server IN", ServerTable/binary>>), + mongoose_rdbms:prepare(mam_muc_make_tombstone, mam_muc_message, [message, room_id, id], <<"UPDATE mam_muc_message SET message = ?, search_body = '' " "WHERE room_id = ? AND id = ?">>), @@ -306,14 +315,31 @@ remove_archive(Acc, HostType, ArcID, _ArcJID) -> mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> F = fun() -> - SubHosts = get_subhosts(HostType, Domain), - {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> - [remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] - end), + case gen_mod:get_module_opt(HostType, ?MODULE, delete_domain_limit) of + infinity -> remove_domain_all(HostType, Domain); + Limit -> remove_domain_batch(HostType, Domain, Limit) + end, Acc - end, + end, mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). +-spec remove_domain_all(host_type(), jid:lserver()) -> any(). +remove_domain_all(HostType, Domain) -> + SubHosts = get_subhosts(HostType, Domain), + {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> + [remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] + end). + +-spec remove_domain_batch(host_type(), jid:lserver(), non_neg_integer()) -> any(). +remove_domain_batch(HostType, Domain, Limit) -> + SubHosts = get_subhosts(HostType, Domain), + DeleteQueries = [mam_muc_remove_domain, mam_muc_remove_domain_users], + DelSubHost = [ mod_mam_utils:incremental_delete_domain(HostType, SubHost, Limit, DeleteQueries, 0) + || SubHost <- SubHosts], + TotalDeleted = lists:sum(DelSubHost), + ?LOG_INFO(#{what => mam_muc_domain_removal_completed, total_records_deleted => TotalDeleted, + domain => Domain, host_type => HostType}). + remove_domain_trans(HostType, MucHost) -> mongoose_rdbms:execute_successfully(HostType, mam_muc_remove_domain, [MucHost]), mongoose_rdbms:execute_successfully(HostType, mam_muc_remove_domain_users, [MucHost]). diff --git a/src/mam/mod_mam_rdbms_arch.erl b/src/mam/mod_mam_rdbms_arch.erl index 29d2c5bd837..73082a35676 100644 --- a/src/mam/mod_mam_rdbms_arch.erl +++ b/src/mam/mod_mam_rdbms_arch.erl @@ -67,9 +67,9 @@ %% Starting and stopping functions for users' archives -spec start(host_type(), gen_mod:module_opts()) -> ok. -start(HostType, _Opts) -> +start(HostType, Opts) -> start_hooks(HostType), - register_prepared_queries(), + register_prepared_queries(Opts), ok. -spec stop(host_type()) -> ok. @@ -125,21 +125,28 @@ hooks(HostType) -> %% ---------------------------------------------------------------------- %% SQL queries -register_prepared_queries() -> +register_prepared_queries(Opts) -> prepare_insert(insert_mam_message, 1), mongoose_rdbms:prepare(mam_archive_remove, mam_message, [user_id], <<"DELETE FROM mam_message " "WHERE user_id = ?">>), + + %% Domain Removal + {MaybeLimitSQL, MaybeLimitMSSQL} = mod_mam_utils:batch_delete_limits(Opts), + IdTable = <<"(SELECT ", MaybeLimitMSSQL/binary, + " id from mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, + ServerTable = <<"(SELECT", MaybeLimitMSSQL/binary, + " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, mongoose_rdbms:prepare(mam_remove_domain, mam_message, ['mam_server_user.server'], <<"DELETE FROM mam_message " - "WHERE user_id IN " - "(SELECT id from mam_server_user WHERE server = ?)">>), + "WHERE user_id IN ", IdTable/binary>>), mongoose_rdbms:prepare(mam_remove_domain_prefs, mam_config, ['mam_server_user.server'], <<"DELETE FROM mam_config " - "WHERE user_id IN " - "(SELECT id from mam_server_user WHERE server = ?)">>), + "WHERE user_id IN ", IdTable/binary>>), mongoose_rdbms:prepare(mam_remove_domain_users, mam_server_user, [server], - <<"DELETE FROM mam_server_user WHERE server = ?">>), + <<"DELETE ", MaybeLimitMSSQL/binary, + " FROM mam_server_user WHERE server IN", ServerTable/binary>>), + mongoose_rdbms:prepare(mam_make_tombstone, mam_message, [message, user_id, id], <<"UPDATE mam_message SET message = ?, search_body = '' " "WHERE user_id = ? AND id = ?">>), @@ -337,15 +344,29 @@ remove_archive(Acc, HostType, ArcID, _ArcJID) -> mongoose_domain_api:remove_domain_acc(). remove_domain(Acc, HostType, Domain) -> F = fun() -> - {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain, [Domain]), - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_prefs, [Domain]), - mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_users, [Domain]) - end), + case gen_mod:get_module_opt(HostType, ?MODULE, delete_domain_limit) of + infinity -> remove_domain_all(HostType, Domain); + Limit -> remove_domain_batch(HostType, Domain, Limit) + end, Acc end, mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). +-spec remove_domain_all(host_type(), jid:lserver()) -> any(). +remove_domain_all(HostType, Domain) -> + {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain, [Domain]), + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_prefs, [Domain]), + mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_users, [Domain]) + end). + +-spec remove_domain_batch(host_type(), jid:lserver(), non_neg_integer()) -> any(). +remove_domain_batch(HostType, Domain, Limit) -> + DeleteQueries = [mam_remove_domain, mam_remove_domain_prefs, mam_remove_domain_users], + TotalDeleted = mod_mam_utils:incremental_delete_domain(HostType, Domain, Limit, DeleteQueries, 0), + ?LOG_INFO(#{what => mam_domain_removal_completed, total_records_deleted => TotalDeleted, + domain => Domain, host_type => HostType}). + %% GDPR logic extract_gdpr_messages(Env, ArcID) -> Filters = [{equal, user_id, ArcID}], diff --git a/src/mam/mod_mam_utils.erl b/src/mam/mod_mam_utils.erl index ecabdbb30b4..f9e8cc213f8 100644 --- a/src/mam/mod_mam_utils.erl +++ b/src/mam/mod_mam_utils.erl @@ -91,6 +91,7 @@ %% Shared logic -export([check_result_for_policy_violation/2, lookup/3, + batch_delete_limits/1, incremental_delete_domain/5, db_message_codec/2, db_jid_codec/2]). -callback extra_fin_element(mongooseim:host_type(), @@ -1234,3 +1235,31 @@ db_jid_codec(HostType, Module) -> -spec db_message_codec(mongooseim:host_type(), module()) -> module(). db_message_codec(HostType, Module) -> gen_mod:get_module_opt(HostType, Module, db_message_format). + +-spec batch_delete_limits(#{delete_domain_limit := infinity | non_neg_integer()}) -> + {binary(), binary()}. +batch_delete_limits(#{delete_domain_limit := infinity}) -> + {<<>>, <<>>}; +batch_delete_limits(#{delete_domain_limit := Limit}) -> + rdbms_queries:get_db_specific_limits_binaries(Limit). + +-spec incremental_delete_domain( + mongooseim:host_type(), jid:lserver(), non_neg_integer(), [atom()], non_neg_integer()) -> + non_neg_integer(). +incremental_delete_domain(_HostType, _Domain, _Limit, [], TotalDeleted) -> + TotalDeleted; +incremental_delete_domain(HostType, Domain, Limit, [Query | MoreQueries] = AllQueries, TotalDeleted) -> + R1 = mongoose_rdbms:execute_successfully(HostType, Query, [Domain]), + case is_removing_done(R1, Limit) of + {done, N} -> + incremental_delete_domain(HostType, Domain, Limit, MoreQueries, N + TotalDeleted); + {remove_more, N} -> + incremental_delete_domain(HostType, Domain, Limit, AllQueries, N + TotalDeleted) + end. + +-spec is_removing_done(LastResult :: {updated, non_neg_integer()}, Limit :: non_neg_integer()) -> + {done | remove_more, non_neg_integer()}. +is_removing_done({updated, N}, Limit) when N < Limit -> + {done, N}; +is_removing_done({updated, N}, _)-> + {remove_more, N}. From 4f884639f83b3023af0eacd46d1153475d38ada4 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 28 Sep 2022 00:58:13 +0200 Subject: [PATCH 088/117] Ensure incremental is tested --- big_tests/tests/domain_removal_SUITE.erl | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/big_tests/tests/domain_removal_SUITE.erl b/big_tests/tests/domain_removal_SUITE.erl index a734b052bef..27c970949cf 100644 --- a/big_tests/tests/domain_removal_SUITE.erl +++ b/big_tests/tests/domain_removal_SUITE.erl @@ -17,6 +17,7 @@ all() -> {group, auth_removal}, {group, cache_removal}, {group, mam_removal}, + {group, mam_removal_incremental}, {group, inbox_removal}, {group, muc_light_removal}, {group, muc_removal}, @@ -35,6 +36,8 @@ groups() -> {cache_removal, [], [cache_removal]}, {mam_removal, [], [mam_pm_removal, mam_muc_removal]}, + {mam_removal_incremental, [], [mam_pm_removal, + mam_muc_removal]}, {inbox_removal, [], [inbox_removal]}, {muc_light_removal, [], [muc_light_removal, muc_light_blocking_removal]}, @@ -93,6 +96,10 @@ group_to_modules(mam_removal) -> MucHost = subhost_pattern(muc_light_helper:muc_host_pattern()), [{mod_mam, mam_helper:config_opts(#{pm => #{}, muc => #{host => MucHost}})}, {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]; +group_to_modules(mam_removal_incremental) -> + MucHost = subhost_pattern(muc_light_helper:muc_host_pattern()), + [{mod_mam, mam_helper:config_opts(#{delete_domain_limit => 1, pm => #{}, muc => #{host => MucHost}})}, + {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]; group_to_modules(muc_light_removal) -> [{mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]; group_to_modules(muc_removal) -> @@ -170,10 +177,11 @@ cache_removal(Config) -> mam_pm_removal(Config) -> F = fun(Alice, Bob) -> - escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)), - escalus:wait_for_stanza(Bob), - mam_helper:wait_for_archive_size(Alice, 1), - mam_helper:wait_for_archive_size(Bob, 1), + N = 3, + [ escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)) || _ <- lists:seq(1, N) ], + escalus:wait_for_stanzas(Bob, N), + mam_helper:wait_for_archive_size(Alice, N), + mam_helper:wait_for_archive_size(Bob, N), run_remove_domain(), mam_helper:wait_for_archive_size(Alice, 0), mam_helper:wait_for_archive_size(Bob, 0) @@ -182,14 +190,15 @@ mam_pm_removal(Config) -> mam_muc_removal(Config0) -> F = fun(Config, Alice) -> + N = 3, Room = muc_helper:fresh_room_name(), MucHost = muc_light_helper:muc_host(), muc_light_helper:create_room(Room, MucHost, alice, [], Config, muc_light_helper:ver(1)), RoomAddr = <>, - escalus:send(Alice, escalus_stanza:groupchat_to(RoomAddr, <<"text">>)), - escalus:wait_for_stanza(Alice), - mam_helper:wait_for_room_archive_size(MucHost, Room, 1), + [ escalus:send(Alice, escalus_stanza:groupchat_to(RoomAddr, <<"text">>)) || _ <- lists:seq(1, N) ], + escalus:wait_for_stanzas(Alice, N), + mam_helper:wait_for_room_archive_size(MucHost, Room, N), run_remove_domain(), mam_helper:wait_for_room_archive_size(MucHost, Room, 0) end, From 23618cb1f317624eaab501211269e950a11b86d3 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 28 Sep 2022 11:03:21 +0200 Subject: [PATCH 089/117] Fix query to make it MySQL compatible MySQL does not allow the update (in this case a delete) to refer to the table being selected, but it allows this double nesting. --- src/mam/mod_mam_muc_rdbms_arch.erl | 4 ++-- src/mam/mod_mam_rdbms_arch.erl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mam/mod_mam_muc_rdbms_arch.erl b/src/mam/mod_mam_muc_rdbms_arch.erl index bc04269d4d9..07d84fe5c5b 100644 --- a/src/mam/mod_mam_muc_rdbms_arch.erl +++ b/src/mam/mod_mam_muc_rdbms_arch.erl @@ -116,8 +116,8 @@ register_prepared_queries(Opts) -> {MaybeLimitSQL, MaybeLimitMSSQL} = mod_mam_utils:batch_delete_limits(Opts), IdTable = <<"(SELECT ", MaybeLimitMSSQL/binary, " id from mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, - ServerTable = <<"(SELECT", MaybeLimitMSSQL/binary, - " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, + ServerTable = <<"(SELECT * FROM (SELECT", MaybeLimitMSSQL/binary, + " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ") as t)">>, mongoose_rdbms:prepare(mam_muc_remove_domain, mam_muc_message, ['mam_server_user.server'], <<"DELETE FROM mam_muc_message " "WHERE room_id IN ", IdTable/binary>>), diff --git a/src/mam/mod_mam_rdbms_arch.erl b/src/mam/mod_mam_rdbms_arch.erl index 73082a35676..1e84beb2b73 100644 --- a/src/mam/mod_mam_rdbms_arch.erl +++ b/src/mam/mod_mam_rdbms_arch.erl @@ -135,8 +135,8 @@ register_prepared_queries(Opts) -> {MaybeLimitSQL, MaybeLimitMSSQL} = mod_mam_utils:batch_delete_limits(Opts), IdTable = <<"(SELECT ", MaybeLimitMSSQL/binary, " id from mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, - ServerTable = <<"(SELECT", MaybeLimitMSSQL/binary, - " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ")">>, + ServerTable = <<"(SELECT * FROM (SELECT", MaybeLimitMSSQL/binary, + " server FROM mam_server_user WHERE server = ? ", MaybeLimitSQL/binary, ") as t)">>, mongoose_rdbms:prepare(mam_remove_domain, mam_message, ['mam_server_user.server'], <<"DELETE FROM mam_message " "WHERE user_id IN ", IdTable/binary>>), @@ -145,7 +145,7 @@ register_prepared_queries(Opts) -> "WHERE user_id IN ", IdTable/binary>>), mongoose_rdbms:prepare(mam_remove_domain_users, mam_server_user, [server], <<"DELETE ", MaybeLimitMSSQL/binary, - " FROM mam_server_user WHERE server IN", ServerTable/binary>>), + " FROM mam_server_user WHERE server IN ", ServerTable/binary>>), mongoose_rdbms:prepare(mam_make_tombstone, mam_message, [message, user_id, id], <<"UPDATE mam_message SET message = ?, search_body = '' " From 03043cbe42a361ff7410dd0755f23ba001dd848a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 28 Sep 2022 12:23:29 +0200 Subject: [PATCH 090/117] Update MySQL version 8.0.20 does not work on arm64, while 8.0.30 does. --- tools/db-versions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/db-versions.sh b/tools/db-versions.sh index e66d6b9444f..3b66b2a7c65 100644 --- a/tools/db-versions.sh +++ b/tools/db-versions.sh @@ -3,7 +3,7 @@ CASSANDRA_VERSION_DEFAULT="3.9" ELASTICSEARCH_VERSION_DEFAULT="5.6.9" -MYSQL_VERSION_DEFAULT="8.0.20" +MYSQL_VERSION_DEFAULT="8.0.30" PGSQL_VERSION_DEFAULT=latest From aa9835d56edc8d31e39dab082609862054188794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Thu, 22 Sep 2022 16:04:18 +0200 Subject: [PATCH 091/117] Refactored hook handlers in mod_event_pusher_hook_translator module --- .../mod_event_pusher_hook_translator.erl | 92 ++++++++++--------- src/mongoose_hooks.erl | 23 +++-- 2 files changed, 67 insertions(+), 48 deletions(-) diff --git a/src/event_pusher/mod_event_pusher_hook_translator.erl b/src/event_pusher/mod_event_pusher_hook_translator.erl index 525d5f5a3b7..e98e3984df0 100644 --- a/src/event_pusher/mod_event_pusher_hook_translator.erl +++ b/src/event_pusher/mod_event_pusher_hook_translator.erl @@ -22,14 +22,11 @@ -export([add_hooks/1, delete_hooks/1]). --export([user_send_packet/4, - filter_local_packet/1, - user_present/2, - user_not_present/5, - unacknowledged_message/2]). - --ignore_xref([filter_local_packet/1, unacknowledged_message/2, user_not_present/5, - user_present/2, user_send_packet/4]). +-export([user_send_packet/3, + filter_local_packet/3, + user_present/3, + user_not_present/3, + unacknowledged_message/3]). %%-------------------------------------------------------------------- %% gen_mod API @@ -37,21 +34,21 @@ -spec add_hooks(mongooseim:host_type()) -> ok. add_hooks(HostType) -> - ejabberd_hooks:add(hooks(HostType)). + gen_hook:add_handlers(hooks(HostType)). -spec delete_hooks(mongooseim:host_type()) -> ok. delete_hooks(HostType) -> - ejabberd_hooks:delete(hooks(HostType)). + gen_hook:delete_handlers(hooks(HostType)). %%-------------------------------------------------------------------- %% Hook callbacks %%-------------------------------------------------------------------- -type routing_data() :: {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}. --spec filter_local_packet(drop) -> drop; - (routing_data()) -> routing_data(). -filter_local_packet(drop) -> - drop; -filter_local_packet({From, To, Acc0, Packet}) -> +-spec filter_local_packet(drop, _, _) -> {ok, drop}; + (routing_data(), _, _) -> {ok, routing_data()}. +filter_local_packet(drop, _, _) -> + {ok, drop}; +filter_local_packet({From, To, Acc0, Packet}, _, _) -> Acc = case chat_type(Acc0) of false -> Acc0; Type -> @@ -60,40 +57,51 @@ filter_local_packet({From, To, Acc0, Packet}) -> NewAcc = mod_event_pusher:push_event(Acc0, Event), merge_acc(Acc0, NewAcc) end, - {From, To, Acc, Packet}. - --spec user_send_packet(mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(), - Packet :: exml:element()) -> mongoose_acc:t(). -user_send_packet(Acc, From, To, Packet = #xmlel{name = <<"message">>}) -> - case chat_type(Acc) of + {ok, {From, To, Acc, Packet}}. + +-spec user_send_packet(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: #{from := jid:jid(), to := jid:jid(), packet := exml:element()}, + Extra :: map(). +user_send_packet(Acc, #{from := From, to := To, packet := Packet = #xmlel{name = <<"message">>}}, _) -> + ResultAcc = case chat_type(Acc) of false -> Acc; Type -> Event = #chat_event{type = Type, direction = in, from = From, to = To, packet = Packet}, NewAcc = mod_event_pusher:push_event(Acc, Event), merge_acc(Acc, NewAcc) - end; -user_send_packet(Acc, _From, _To, _Packet) -> - Acc. - --spec user_present(mongoose_acc:t(), UserJID :: jid:jid()) -> mongoose_acc:t(). -user_present(Acc, #jid{} = UserJID) -> + end, + {ok, ResultAcc}; +user_send_packet(Acc, _, _) -> + {ok, Acc}. + +-spec user_present(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: #{jid := jid:jid()}, + Extra :: map(). +user_present(Acc, #{jid := UserJID = #jid{}}, _) -> Event = #user_status_event{jid = UserJID, status = online}, NewAcc = mod_event_pusher:push_event(Acc, Event), - merge_acc(Acc, NewAcc). + {ok, merge_acc(Acc, NewAcc)}. --spec user_not_present(mongoose_acc:t(), User :: jid:luser(), Server :: jid:lserver(), - Resource :: jid:lresource(), Status :: any()) -> mongoose_acc:t(). -user_not_present(Acc, LUser, LServer, LResource, _Status) -> - UserJID = jid:make_noprep(LUser, LServer, LResource), +-spec user_not_present(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: #{jid := jid:jid()}, + Extra :: map(). +user_not_present(Acc, #{jid := UserJID}, _) -> Event = #user_status_event{jid = UserJID, status = offline}, NewAcc = mod_event_pusher:push_event(Acc, Event), - merge_acc(Acc, NewAcc). + {ok, merge_acc(Acc, NewAcc)}. -unacknowledged_message(Acc, Jid) -> +-spec unacknowledged_message(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: #{jid := jid:jid()}, + Extra :: map(). +unacknowledged_message(Acc, #{jid := Jid}, _) -> Event = #unack_msg_event{to = Jid}, NewAcc = mod_event_pusher:push_event(Acc, Event), - merge_acc(Acc, NewAcc). + {ok, merge_acc(Acc, NewAcc)}. %%-------------------------------------------------------------------- %% Helpers @@ -115,13 +123,13 @@ merge_acc(Acc, EventPusherAcc) -> NS = mongoose_acc:get(event_pusher, EventPusherAcc), mongoose_acc:set_permanent(event_pusher, NS, Acc). --spec hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. +-spec hooks(mongooseim:host_type()) -> [gen_hook:hook_tuple()]. hooks(HostType) -> [ - {filter_local_packet, HostType, ?MODULE, filter_local_packet, 80}, - {unset_presence_hook, HostType, ?MODULE, user_not_present, 90}, - {user_available_hook, HostType, ?MODULE, user_present, 90}, - {user_send_packet, HostType, ?MODULE, user_send_packet, 90}, - {rest_user_send_packet, HostType, ?MODULE, user_send_packet, 90}, - {unacknowledged_message, HostType, ?MODULE, unacknowledged_message, 90} + {filter_local_packet, HostType, fun ?MODULE:filter_local_packet/3, #{}, 80}, + {unset_presence_hook, HostType, fun ?MODULE:user_not_present/3, #{}, 90}, + {user_available_hook, HostType, fun ?MODULE:user_present/3, #{}, 90}, + {user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 90}, + {rest_user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 90}, + {unacknowledged_message, HostType, fun ?MODULE:unacknowledged_message/3, #{}, 90} ]. diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 474d6f415a0..98580e50f99 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -267,7 +267,7 @@ failed_to_store_message(Acc) -> Result :: drop | filter_packet_acc(). filter_local_packet(FilterAcc = {_From, _To, Acc, _Packet}) -> HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(filter_local_packet, HostType, FilterAcc, []). + run_hook_for_host_type(filter_local_packet, HostType, FilterAcc, #{args => []}). %%% @doc The `filter_packet' hook is called to filter out %%% stanzas routed with `mongoose_router_global'. @@ -392,8 +392,11 @@ resend_offline_messages_hook(Acc, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). rest_user_send_packet(Acc, From, To, Packet) -> + Params = #{from => From, to => To, packet => Packet}, + Args = [From, To, Packet], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(rest_user_send_packet, HostType, Acc, [From, To, Packet]). + run_hook_for_host_type(rest_user_send_packet, HostType, Acc, ParamsWithLegacyArgs). %%% @doc The `session_cleanup' hook is called when sm backend cleans up a user's session. -spec session_cleanup(Server, Acc, User, Resource, SID) -> Result when @@ -458,8 +461,11 @@ unregister_subhost(LDomain) -> JID :: jid:jid(), Result :: mongoose_acc:t(). user_available_hook(Acc, JID) -> + Params = #{jid => JID}, + Args = [JID], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(user_available_hook, HostType, Acc, [JID]). + run_hook_for_host_type(user_available_hook, HostType, Acc, ParamsWithLegacyArgs). %%% @doc The `user_ping_response' hook is called when a user responds to a ping. -spec user_ping_response(HostType, Acc, JID, Response, TDelta) -> Result when @@ -513,8 +519,11 @@ user_sent_keep_alive(HostType, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). user_send_packet(Acc, From, To, Packet) -> + Params = #{from => From, to => To, packet => Packet}, + Args = [From, To, Packet], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(user_send_packet, HostType, Acc, [From, To, Packet]). + run_hook_for_host_type(user_send_packet, HostType, Acc, ParamsWithLegacyArgs). %%% @doc The `vcard_set' hook is called to inform that the vcard %%% has been set in mod_vcard backend. @@ -775,9 +784,11 @@ sm_remove_connection_hook(Acc, SID, JID, Info, Reason) -> Result :: mongoose_acc:t(). unset_presence_hook(Acc, JID, Status) -> #jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, + Params = #{jid => JID}, + Args = [LUser, LServer, LResource, Status], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(unset_presence_hook, HostType, Acc, - [LUser, LServer, LResource, Status]). + run_hook_for_host_type(unset_presence_hook, HostType, Acc, ParamsWithLegacyArgs). -spec xmpp_bounce_message(Acc) -> Result when Acc :: mongoose_acc:t(), From ccc1e3d266efa559965d6a4f0234a879f485635e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Wed, 28 Sep 2022 10:22:14 +0200 Subject: [PATCH 092/117] Removed From, To, Packet from params in user_send_packet and rest_user_send_packet hook --- src/event_pusher/mod_event_pusher_hook_translator.erl | 5 +++-- src/mongoose_hooks.erl | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/event_pusher/mod_event_pusher_hook_translator.erl b/src/event_pusher/mod_event_pusher_hook_translator.erl index e98e3984df0..647c903c66f 100644 --- a/src/event_pusher/mod_event_pusher_hook_translator.erl +++ b/src/event_pusher/mod_event_pusher_hook_translator.erl @@ -61,9 +61,10 @@ filter_local_packet({From, To, Acc0, Packet}, _, _) -> -spec user_send_packet(Acc, Args, Extra) -> {ok, Acc} when Acc :: mongoose_acc:t(), - Args :: #{from := jid:jid(), to := jid:jid(), packet := exml:element()}, + Args :: map(), Extra :: map(). -user_send_packet(Acc, #{from := From, to := To, packet := Packet = #xmlel{name = <<"message">>}}, _) -> +user_send_packet(Acc, _, _) -> + {From, To, Packet} = mongoose_acc:packet(Acc), ResultAcc = case chat_type(Acc) of false -> Acc; Type -> diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 98580e50f99..edc62811bac 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -392,7 +392,7 @@ resend_offline_messages_hook(Acc, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). rest_user_send_packet(Acc, From, To, Packet) -> - Params = #{from => From, to => To, packet => Packet}, + Params = #{}, Args = [From, To, Packet], ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), @@ -519,7 +519,7 @@ user_sent_keep_alive(HostType, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). user_send_packet(Acc, From, To, Packet) -> - Params = #{from => From, to => To, packet => Packet}, + Params = #{}, Args = [From, To, Packet], ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), HostType = mongoose_acc:host_type(Acc), From 054ecf264755a0442d24d5d2570df29a3f04fc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Thu, 29 Sep 2022 12:13:24 +0200 Subject: [PATCH 093/117] Review changes --- .../mod_event_pusher_hook_translator.erl | 37 +++++++++++-------- src/mongoose_hooks.erl | 3 +- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/event_pusher/mod_event_pusher_hook_translator.erl b/src/event_pusher/mod_event_pusher_hook_translator.erl index 647c903c66f..e0ef8b96f07 100644 --- a/src/event_pusher/mod_event_pusher_hook_translator.erl +++ b/src/event_pusher/mod_event_pusher_hook_translator.erl @@ -51,11 +51,7 @@ filter_local_packet(drop, _, _) -> filter_local_packet({From, To, Acc0, Packet}, _, _) -> Acc = case chat_type(Acc0) of false -> Acc0; - Type -> - Event = #chat_event{type = Type, direction = out, - from = From, to = To, packet = Packet}, - NewAcc = mod_event_pusher:push_event(Acc0, Event), - merge_acc(Acc0, NewAcc) + Type -> push_chat_event(Acc0, Type, {From, To, Packet}, out) end, {ok, {From, To, Acc, Packet}}. @@ -64,18 +60,14 @@ filter_local_packet({From, To, Acc0, Packet}, _, _) -> Args :: map(), Extra :: map(). user_send_packet(Acc, _, _) -> - {From, To, Packet} = mongoose_acc:packet(Acc), - ResultAcc = case chat_type(Acc) of - false -> Acc; - Type -> - Event = #chat_event{type = Type, direction = in, - from = From, to = To, packet = Packet}, - NewAcc = mod_event_pusher:push_event(Acc, Event), - merge_acc(Acc, NewAcc) + Packet = mongoose_acc:packet(Acc), + ChatType = chat_type(Acc), + ResultAcc = if + Packet == undefined -> Acc; + ChatType == false -> Acc; + true -> push_chat_event(Acc, ChatType, Packet, in) end, - {ok, ResultAcc}; -user_send_packet(Acc, _, _) -> - {ok, Acc}. + {ok, ResultAcc}. -spec user_present(Acc, Args, Extra) -> {ok, Acc} when Acc :: mongoose_acc:t(), @@ -108,6 +100,19 @@ unacknowledged_message(Acc, #{jid := Jid}, _) -> %% Helpers %%-------------------------------------------------------------------- +-spec push_chat_event(Acc, Type, {From, To, Packet}, Direction) -> Acc when + Acc :: mongoose_acc:t(), + Type :: chat | groupchat | headline | normal | false, + From :: jid:jid(), + To :: jid:jid(), + Packet :: exml:element(), + Direction :: in | out. +push_chat_event(Acc, Type, {From, To, Packet}, Direction) -> + Event = #chat_event{type = Type, direction = Direction, + from = From, to = To, packet = Packet}, + NewAcc = mod_event_pusher:push_event(Acc, Event), + merge_acc(Acc, NewAcc). + -spec chat_type(mongoose_acc:t()) -> chat | groupchat | headline | normal | false. chat_type(Acc) -> case mongoose_acc:stanza_type(Acc) of diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index edc62811bac..d3158ba318c 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -267,7 +267,8 @@ failed_to_store_message(Acc) -> Result :: drop | filter_packet_acc(). filter_local_packet(FilterAcc = {_From, _To, Acc, _Packet}) -> HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(filter_local_packet, HostType, FilterAcc, #{args => []}). + ParamsWithLegacyArgs = ejabberd_hooks:add_args(#{}, []), + run_hook_for_host_type(filter_local_packet, HostType, FilterAcc, ParamsWithLegacyArgs). %%% @doc The `filter_packet' hook is called to filter out %%% stanzas routed with `mongoose_router_global'. From 572b0d33abc788532e38890bd169b452dbdbb369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Fri, 30 Sep 2022 09:48:13 +0200 Subject: [PATCH 094/117] Refactored hook handler in mod_event_pusher_push module --- src/event_pusher/mod_event_pusher_push.erl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/event_pusher/mod_event_pusher_push.erl b/src/event_pusher/mod_event_pusher_push.erl index b6909a7c236..a065e3f3403 100644 --- a/src/event_pusher/mod_event_pusher_push.erl +++ b/src/event_pusher/mod_event_pusher_push.erl @@ -45,9 +45,7 @@ -export([is_virtual_pubsub_host/3]). -export([disable_node/4]). --ignore_xref([ - iq_handler/4, remove_user/3 -]). +-ignore_xref([iq_handler/4]). %% Types -type publish_service() :: {PubSub :: jid:jid(), Node :: pubsub_node(), Form :: form()}. @@ -68,7 +66,7 @@ start(HostType, Opts) -> mod_event_pusher_push_backend:init(HostType, Opts), mod_event_pusher_push_plugin:init(HostType, Opts), init_iq_handlers(HostType, Opts), - ejabberd_hooks:add(remove_user, HostType, ?MODULE, remove_user, 90), + gen_hook:add_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90), ok. start_pool(HostType, #{wpool := WpoolOpts}) -> @@ -82,7 +80,7 @@ init_iq_handlers(HostType, #{iqdisc := IQDisc}) -> -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - ejabberd_hooks:delete(remove_user, HostType, ?MODULE, remove_user, 90), + gen_hook:delete_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90), gen_iq_handler:remove_iq_handler(ejabberd_sm, HostType, ?NS_PUSH), gen_iq_handler:remove_iq_handler(ejabberd_local, HostType, ?NS_PUSH), @@ -128,12 +126,14 @@ push_event(Acc, _) -> %%-------------------------------------------------------------------- %% Hooks and IQ handlers %%-------------------------------------------------------------------- --spec remove_user(Acc :: mongoose_acc:t(), LUser :: jid:luser(), LServer :: jid:lserver()) -> - mongoose_acc:t(). -remove_user(Acc, LUser, LServer) -> +-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Params :: #{jid := jid:jid()}, + Extra :: map(). +remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) -> R = mod_event_pusher_push_backend:disable(LServer, jid:make_noprep(LUser, LServer, <<>>)), mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, LUser, LServer}), - Acc. + {ok, Acc}. -spec iq_handler(From :: jid:jid(), To :: jid:jid(), Acc :: mongoose_acc:t(), IQ :: jlib:iq()) -> From b88ec048690d00c827e8f476bb205e82b963cc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Mon, 3 Oct 2022 13:59:50 +0200 Subject: [PATCH 095/117] Unified REST API docs (#3780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move Inbox REST API docs to Swagger * Remove mod_*commands from the docs * Move the swagger docs for the dynamic domains Merge the docs with the backend ones, just as the HTTP handlers are merged. * Refer to the API docs for domain management * Update the API listener docs * Remove the deleted /commands endpoint from docs * Move metrics API docs to the Admin REST Swagger * Return valid JSON value for Admin REST API errors Previously it was plain text, but the Content-Type was application/json This caused issues with the interactive Swagger UI - it displayed "no response from server". Now it displays the errors correctly. Tests don't need changing, because they are decoding JSON automatically, and the result is the same text as before. * Remove JSON error examples for domains They got outdated, and it is difficult to keep the up to date. Moreover, for the remaining operations there are no error examples. * Update migration guide with the REST API changes Co-authored-by: Paweł Chrząszcz --- doc/configuration/Modules.md | 13 - doc/configuration/general.md | 3 +- doc/configuration/listen.md | 152 +++----- doc/developers-guide/domain_management.md | 86 +---- .../mod_muc_light_developers_guide.md | 5 - doc/migrations/5.1.0_6.0.0.md | 19 +- doc/modules/mod_commands.md | 79 ----- doc/modules/mod_inbox.md | 2 +- doc/modules/mod_inbox_commands.md | 47 --- doc/modules/mod_muc_commands.md | 65 ---- doc/modules/mod_muc_light_commands.md | 69 ---- doc/rest-api/Administration-backend.md | 76 +--- .../Administration-backend_swagger.yml | 332 ++++++++++++++++-- doc/rest-api/Client-frontend.md | 6 +- doc/rest-api/Dynamic-domains.md | 37 -- doc/rest-api/Dynamic-domains_swagger.yml | 146 -------- doc/rest-api/Metrics-backend.md | 136 ------- doc/rest-api/Metrics-backend_swagger.yml | 0 doc/swagger/index.html | 3 - doc/user-guide/Features.md | 2 +- mkdocs.yml | 6 - src/mongoose_admin_api/mongoose_admin_api.erl | 2 +- 22 files changed, 397 insertions(+), 889 deletions(-) delete mode 100644 doc/modules/mod_commands.md delete mode 100644 doc/modules/mod_inbox_commands.md delete mode 100644 doc/modules/mod_muc_commands.md delete mode 100644 doc/modules/mod_muc_light_commands.md delete mode 100644 doc/rest-api/Dynamic-domains.md delete mode 100644 doc/rest-api/Dynamic-domains_swagger.yml delete mode 100644 doc/rest-api/Metrics-backend.md delete mode 100644 doc/rest-api/Metrics-backend_swagger.yml diff --git a/doc/configuration/Modules.md b/doc/configuration/Modules.md index e43136acb7d..5b17c9fcca8 100644 --- a/doc/configuration/Modules.md +++ b/doc/configuration/Modules.md @@ -70,10 +70,6 @@ This module tightly cooperates with [mod_pubsub](../modules/mod_pubsub.md) in or ### [mod_carboncopy](../modules/mod_carboncopy.md) Implements [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html) in order to keep all IM clients for a user engaged in a real-time conversation by carbon-copying all inbound and outbound messages to all interested resources (Full JIDs). -### [mod_commands](../modules/mod_commands.md) -A central gateway providing access to a subset of MongooseIM functions by channels other than XMPP. -Commands defined there are currently accessible via REST API. - ### [mod_csi](../modules/mod_csi.md) Enables the [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) functionality. @@ -106,9 +102,6 @@ Implements [XEP-0363: HTTP File Upload](https://xmpp.org/extensions/xep-0363.htm ### [mod_inbox](../modules/mod_inbox.md) Implements custom inbox XEP -### [mod_inbox_commands](../modules/mod_inbox_commands.md) -Exposes administrative commands for the [inbox](../modules/mod_inbox.md) - ### [mod_global_distrib](../modules/mod_global_distrib.md) Enables sharing a single XMPP domain between distinct datacenters (**experimental**). @@ -128,18 +121,12 @@ Implements [XEP-0313: Message Archive Management](http://xmpp.org/extensions/xep Implements [XEP-0045: Multi-User Chat](http://xmpp.org/extensions/xep-0045.html), for a featureful multi-user text chat (group chat), whereby multiple XMPP users can exchange messages in the context of a chat room. It is tightly coupled with user presence in chat rooms. -### [mod_muc_commands](../modules/mod_muc_commands.md) -Provides `mod_muc` related `mongoose_commands`, accessible via the client REST API. - ### [mod_muc_log](../modules/mod_muc_log.md) Implements a logging subsystem for [mod_muc](../modules/mod_muc.md). ### [mod_muc_light](../modules/mod_muc_light.md) Implements [XEP Multi-User Chat Light](https://xmpp.org/extensions/inbox/muc-light.html). -### [mod_muc_light_commands](../modules/mod_muc_light_commands.md) -Provides `mod_muc_light` related `mongoose_commands`, accessible via client REST API. - ### [mod_offline](../modules/mod_offline.md) Provides an offline messages storage that is compliant with [XEP-0160: Best Practices for Handling Offline Messages](http://xmpp.org/extensions/xep-0160.html). diff --git a/doc/configuration/general.md b/doc/configuration/general.md index 99120ece6b6..6adc440ab1e 100644 --- a/doc/configuration/general.md +++ b/doc/configuration/general.md @@ -44,8 +44,7 @@ In order to configure these hosts independently, use the [`host_config` section] This is the list of names for the types of hosts that will serve dynamic XMPP domains. Each host type can be seen as a label for a group of independent domains that use the same server configuration. -In order to configure these host types independently, use the [`host_config` section](./host_config.md). -The domains can be added or removed dynamically via the [dynamic domains REST API](../rest-api/Dynamic-domains.md). +In order to configure these host types independently, use the [`host_config` section](./host_config.md). The domains can be added or removed dynamically with the [command line interface](../../developers-guide/domain_management#command-line-interface) or using the [API](../../developers-guide/domain_management#api). If you use the host type mechanism, make sure you only configure modules which support dynamic domains in the [`modules`](./Modules.md) or [`host_config.modules`](./host_config.md#host_configmodules) sections. MongooseIM will **not** start otherwise. diff --git a/doc/configuration/listen.md b/doc/configuration/listen.md index 95b87dc1bf5..fa125b48ba0 100644 --- a/doc/configuration/listen.md +++ b/doc/configuration/listen.md @@ -399,7 +399,7 @@ The shaper named `fast` needs to be defined in the `shaper` section. ## HTTP-based services: `[[listen.http]]` -Manages all HTTP-based services, such as BOSH (HTTP long-polling), WebSocket and REST. +Manages all HTTP-based services, such as BOSH (HTTP long-polling), WebSocket, GraphQL and REST. It uses the [Cowboy](https://ninenines.eu/docs/en/cowboy/2.6/manual) web server. Recommended port number: 5280 for BOSH/WS. @@ -411,7 +411,7 @@ There are the following options for each of the HTTP listeners: * `mod_bosh` - for [BOSH](https://xmpp.org/extensions/xep-0124.html) connections, * `mod_websockets` - for [WebSocket](https://tools.ietf.org/html/rfc6455) connections, * `mongoose_graphql_cowboy_handler` - for GraphQL API, - * `mongoose_api_admin`, `mongoose_api_client`(obsolete), `mongoose_client_api`, `mongoose_domain_handler`, `mongoose_api` - for REST API. + * `mongoose_admin_api`, `mongoose_client_api` - for REST API. These types are described below in more detail. The double-bracket syntax is used because there can be multiple handlers of a given type, so for each type there is a TOML array of one or more tables (subsections). @@ -505,9 +505,9 @@ The following options are supported for this handler: Specifies the schema endpoint: -* `admin` - Endpoint with the admin commands. A global admin has permission to execute all commands. See the recommended configuration - [Example 5](#example-5-admin-graphql-api). -* `domain_admin` - Endpoint with the admin commands. A domain admin has permission to execute only commands with the owned domain. See the recommended configuration - [Example 6](#example-6-domain-admin-graphql-api). -* `user` - Endpoint with the user commands. Used to manage the authorized user. See the recommended configuration - [Example 7](#example-7-user-graphql-api). +* `admin` - Endpoint with the admin commands. A global admin has permission to execute all commands. See the recommended configuration - [Example 2](#example-2-admin-graphql-api). +* `domain_admin` - Endpoint with the admin commands. A domain admin has permission to execute only commands with the owned domain. See the recommended configuration - [Example 3](#example-3-domain-admin-graphql-api). +* `user` - Endpoint with the user commands. Used to manage the authorized user. See the recommended configuration - [Example 4](#example-4-user-graphql-api). #### `listen.http.handlers.mongoose_graphql_cowboy_handler.username` - only for `admin` * **Syntax:** string @@ -523,40 +523,47 @@ When set, enables authentication for the admin API, otherwise it is disabled. Re Required to enable authentication for the admin API. -### Handler types: REST API - Admin - `mongoose_api_admin` +### Handler types: REST API - Admin - `mongoose_admin_api` -The recommended configuration is shown in [Example 2](#example-2-admin-api) below. +The recommended configuration is shown in [Example 5](#example-5-admin-rest-api) below. For more information about the API, see the [REST interface](../rest-api/Administration-backend.md) documentation. The following options are supported for this handler: -#### `listen.http.handlers.mongoose_api_admin.username` +#### `listen.http.handlers.mongoose_admin_api.username` * **Syntax:** string * **Default:** not set * **Example:** `username = "admin"` When set, enables authentication for the admin API, otherwise it is disabled. Requires setting `password`. -#### `listen.http.handlers.mongoose_api_admin.password` +#### `listen.http.handlers.mongoose_admin_api.password` * **Syntax:** string * **Default:** not set * **Example:** `password = "secret"` Required to enable authentication for the admin API. +#### `listen.http.handlers.mongoose_admin_api.handlers` +* **Syntax:** array of strings. Allowed values: `"contacts"`, `"users"`, `"sessions"`, `"messages"`, `"stanzas"`, `"muc_light"`, `"muc"`, `"inbox"`, `"domain"`, `"metrics"`. +* **Default:** all API handler modules enabled +* **Example:** `handlers = ["domain"]` + +The admin API consists of several handler modules, each of them implementing a subset of the functionality. +By default all modules are enabled, so you don't need to change this option. + ### Handler types: REST API - Client - `mongoose_client_api` -The recommended configuration is shown in [Example 3](#example-3-client-api) below. +The recommended configuration is shown in [Example 6](#example-6-client-rest-api) below. Please refer to [REST interface](../rest-api/Client-frontend.md) documentation for more information. The following options are supported for this handler: #### `listen.http.handlers.mongoose_client_api.handlers` -* **Syntax:** array of strings - Erlang modules +* **Syntax:** array of strings. Allowed values: `"sse"`, `"messages"`, `"contacts"`, `"rooms"`, `"rooms_config"`, `"rooms_users"`, `"rooms_messages"`. * **Default:** all API handler modules enabled -* **Example:** `handlers = ["mongoose_client_api_messages", "mongoose_client_api_sse"]` +* **Example:** `handlers = ["messages", "sse"]` -The client API consists of several modules, each of them implementing a subset of the functionality. +The client API consists of several handler modules, each of them implementing a subset of the functionality. By default all modules are enabled, so you don't need to change this option. -For a list of allowed modules, you need to consult the [source code](https://github.com/esl/MongooseIM/blob/master/src/mongoose_client_api/mongoose_client_api.erl). #### `listen.http.handlers.mongoose_client_api.docs` * **Syntax:** boolean @@ -566,38 +573,6 @@ For a list of allowed modules, you need to consult the [source code](https://git The Swagger documentation of the client API is hosted at the `/api-docs` path. You can disable the hosted documentation by setting this option to `false`. -### Handler types: REST API - Domain management - `mongoose_domain_handler` - -The recommended configuration is shown in [Example 4](#example-4-domain-api) below. -This handler enables dynamic domain management for different host types. -For more information about the API, see the [REST interface](../rest-api/Dynamic-domains.md) documentation. -The following options are supported for this handler: - -#### `listen.http.handlers.mongoose_domain_handler.username` -* **Syntax:** string -* **Default:** not set -* **Example:** `username = "admin"` - -When set, enables authentication to access this endpoint. Requires setting password. - -#### `listen.http.handlers.mongoose_domain_handler.password` -* **Syntax:** string -* **Default:** not set -* **Example:** `password = "secret"` - -Required to enable authentication for this endpoint. - -### Handler types: Metrics API (obsolete) - `mongoose_api` - -REST API for accessing the internal MongooseIM metrics. -Please refer to the [REST interface to metrics](../rest-api/Metrics-backend.md) page for more information. -The following option is required: - -#### `listen.http.handlers.mongoose_api.handlers` -* **Syntax:** array of strings - Erlang modules -* **Default:** all API handler modules enabled -* **Example:** `handlers = ["mongoose_api_metrics"]` - ### Transport options The options listed below are used to modify the HTTP transport settings. @@ -661,10 +636,9 @@ The following listener accepts BOSH and WebSocket connections and has TLS config path = "/ws-xmpp" ``` -#### Example 2. Admin API +#### Example 2. Admin GraphQL API -REST API for administration, the listener is bound to `127.0.0.1` for increased security. -The number of acceptors and connections is specified (reduced). +GraphQL API for administration, the listener is bound to 127.0.0.1 for increased security. The number of acceptors and connections is specified (reduced). ```toml [[listen.http]] @@ -673,47 +647,53 @@ The number of acceptors and connections is specified (reduced). transport.num_acceptors = 5 transport.max_connections = 10 - [[listen.http.handlers.mongoose_api_admin]] + [[listen.http.handlers.mongoose_graphql_cowboy_handler]] host = "localhost" - path = "/api" + path = "/api/graphql" + schema_endpoint = "admin" + username = "admin" + password = "secret" ``` -#### Example 3. Client API +#### Example 3. Domain Admin GraphQL API -REST API for clients. +GraphQL API for the domain admin. ```toml [[listen.http]] - port = 8089 + ip_address = "0.0.0.0" + port = 5041 + transport.num_acceptors = 10 transport.max_connections = 1024 - protocol.compress = true - [[listen.http.handlers.mongoose_client_api]] + [[listen.http.handlers.mongoose_graphql_cowboy_handler]] host = "_" - path = "/api" + path = "/api/graphql" + schema_endpoint = "domain_admin" ``` -#### Example 4. Domain API +#### Example 4. User GraphQL API -REST API for domain management. +GraphQL API for the user. ```toml [[listen.http]] - ip_address = "127.0.0.1" - port = 8088 + ip_address = "0.0.0.0" + port = 5061 transport.num_acceptors = 10 transport.max_connections = 1024 - [[listen.http.handlers.mongoose_domain_handler]] - host = "localhost" - path = "/api" - username = "admin" - password = "secret" + [[listen.http.handlers.mongoose_graphql_cowboy_handler]] + host = "_" + path = "/api/graphql" + schema_endpoint = "user" ``` -#### Example 5. Admin GraphQL API +#### Example 5. Admin REST API -GraphQL API for administration, the listener is bound to 127.0.0.1 for increased security. The number of acceptors and connections is specified (reduced). +REST API for administration, the listener is bound to `127.0.0.1` for increased security. +The number of acceptors and connections is specified (reduced). +Basic HTTP authentication is used as well. ```toml [[listen.http]] @@ -722,44 +702,24 @@ GraphQL API for administration, the listener is bound to 127.0.0.1 for increased transport.num_acceptors = 5 transport.max_connections = 10 - [[listen.http.handlers.mongoose_graphql_cowboy_handler]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" - path = "/api/graphql" - schema_endpoint = "admin" + path = "/api" username = "admin" password = "secret" ``` -#### Example 6. Domain Admin GraphQL API +#### Example 6. Client REST API -GraphQL API for the domain admin. - -```toml -[[listen.http]] - ip_address = "0.0.0.0" - port = 5041 - transport.num_acceptors = 10 - transport.max_connections = 1024 - - [[listen.http.handlers.mongoose_graphql_cowboy_handler]] - host = "_" - path = "/api/graphql" - schema_endpoint = "domain_admin" -``` - -#### Example 7. User GraphQL API - -GraphQL API for the user. +REST API for clients. ```toml [[listen.http]] - ip_address = "0.0.0.0" - port = 5061 - transport.num_acceptors = 10 + port = 8089 transport.max_connections = 1024 + protocol.compress = true - [[listen.http.handlers.mongoose_graphql_cowboy_handler]] + [[listen.http.handlers.mongoose_client_api]] host = "_" - path = "/api/graphql" - schema_endpoint = "user" + path = "/api" ``` diff --git a/doc/developers-guide/domain_management.md b/doc/developers-guide/domain_management.md index 957cac703dc..0d90930a0e4 100644 --- a/doc/developers-guide/domain_management.md +++ b/doc/developers-guide/domain_management.md @@ -112,84 +112,9 @@ Please note, that [`mod_auth_token`](../modules/mod_auth_token.md) is the only e Described in the [`services` section](../configuration/Services.md#service_domain_db). -## REST API - -Provides API for adding/removing and enabling/disabling domains over HTTP. -Implemented by `mongoose_domain_handler` module. - -Configuration described in the [`listen` section](../configuration/listen.md#handler-types-rest-api---domain-management---mongoose_domain_handler). - -REST API is documented using Swagger in [REST API for dynamic domains management](../rest-api/Dynamic-domains.md). -Below are examples of how to use this API with the help of `curl`: - -### Add domain - -```bash -curl -v -X PUT "http://localhost:8088/api/domains/example.db" \ - --user admin:secret \ - -H 'content-type: application/json' \ - -d '{"host_type": "type1"}' -``` - -Result codes: - -* 204 - Domain was successfully inserted. -* 400 - Bad request. -* 403 - DB service disabled, or the host type is unknown. -* 409 - Domain already exists with a different host type. -* 500 - Other errors. - -Example of the result body with a failure reason: - -``` -{"what":"unknown host type"} -``` - -Check the `src/domain/mongoose_domain_handler.erl` file for the exact values of the `what` field if needed. - -### Remove domain - -You must provide the domain's host type inside the body: - -```bash -curl -v -X DELETE "http://localhost:8088/api/domains/example.db" \ - --user admin:secret \ - -H 'content-type: application/json' \ - -d '{"host_type": "type1"}' -``` - -Result codes: - -* 204 - The domain is removed or not found. -* 403 - One of: - * the domain is static. - * the DB service is disabled. - * the host type is wrong (does not match the host type in the database). - * the host type is unknown. -* 500 - Other errors. - -### Enable/disable domain - -Provide `{"enabled": true}` as a body to enable a domain. -Provide `{"enabled": false}` as a body to disable a domain. - -```bash -curl -v -X PATCH "http://localhost:8088/api/domains/example.db" \ - --user admin:secret \ - -H 'content-type: application/json' \ - -d '{"enabled": true}' -``` - -Result codes: - -* 204 - Domain was successfully updated. -* 403 - Domain is static, or the service is disabled. -* 404 - Domain not found. -* 500 - Other errors. - ## Command Line Interface -Domain management commands are grouped into the `domain` category. +You can manage the domains with the `mongooseimctl` command. Some examples are provided below: ### Add domain: @@ -214,3 +139,12 @@ Domain management commands are grouped into the `domain` category. ``` ./mongooseimctl domain enableDomain --domain example.com ``` + +Run `./mongooseimctl domain` to get more information about all supported operations. + +## API + +You can manage domains with one of our API's: + +* The [GraphQL API](../../graphql-api/Admin-GraphQL) has the same funtionality as the command line interface. The queries and mutations for domains are grouped under the `domain` category. +* The [REST API](../../rest-api/Administration-backend) (deprecated) supports domain management as well. See Dynamic Domains for details. diff --git a/doc/developers-guide/mod_muc_light_developers_guide.md b/doc/developers-guide/mod_muc_light_developers_guide.md index cce0c7ab94c..82ef8456f27 100644 --- a/doc/developers-guide/mod_muc_light_developers_guide.md +++ b/doc/developers-guide/mod_muc_light_developers_guide.md @@ -32,11 +32,6 @@ All source files can be found in `src/muc_light/`. An implementation of a modern MUC Light protocol, described in the XEP. Supports all MUC Light features. -* `mod_muc_light_commands.erl` - - MUC Light-related commands. - They are registered in the `mongoose_commands` module, so they are available via the REST API. - * `mod_muc_light_db_backend.erl` A behaviour implemented by database backends for the MUC Light extension. diff --git a/doc/migrations/5.1.0_6.0.0.md b/doc/migrations/5.1.0_6.0.0.md index 3010083757a..4fac9218b6d 100644 --- a/doc/migrations/5.1.0_6.0.0.md +++ b/doc/migrations/5.1.0_6.0.0.md @@ -1,18 +1,26 @@ -## Configuration +## Module configuration -The `mod_mam_meta` module is now named `mod_mam` for simplicity, so if you are using this module, you need to update the module name in `mongooseim.toml`. +* The `mod_mam_meta` module is now named `mod_mam` for simplicity, so if you are using this module, you need to update the module name in `mongooseim.toml`. +* `mod_commands`, `mod_inbox_commands`, `mod_muc_commands` and `mod_commands` are removed. Their functionality is now fully covered by [`mongoose_admin_api`](../../configuration/listen/#handler-types-rest-api-admin-mongoose_admin_api). You need to delete these modules from `mongooseim.toml`. ## Metrics The `mod_mam` backend module is now named `mod_mam_pm` for consistency with `mod_mam_muc`. As a result, the backend metrics have updated names, i.e. each `[backends, mod_mam, Metric]` name is changed to `[backends, mod_mam_pm, Metric]`, where `Metric` can be `lookup` or `archive`. -## Rest API +## REST API -All the backend administration endpoints for `mod_muc_light` require now `XMPPMUCHost` (MUC subdomain) instead of `XMPPHost` (domain) and `roomID` instead of `roomName`. +The whole REST API has been unified and simplified. There are now only two REST API handlers that you can configure in the `listen` section of `mongooseim.toml`: + +- [`mongoose_admin_api`](../../configuration/listen/#handler-types-rest-api-admin-mongoose_admin_api) handles the administrative API, +- [`mongoose_client_api`](../../configuration/listen/#handler-types-rest-api-client-mongoose_client_api) handles the client-facing API. + +You need to remove the references to the obsolete handlers (`mongoose_api_client`, `mongoose_api_admin`, `mongoose_api`, `mongoose_domain_handler`) from your configuration file. + +Additionally, all the backend administration endpoints for `mod_muc_light` require now `XMPPMUCHost` (MUC subdomain) instead of `XMPPHost` (domain) and `roomID` instead of `roomName`. For some endpoints, the response messages may be slightly different because of the unification with other APIs. -## CTL +## Command Line Interface For some commands, the response messages may be slightly different because of the unification with other APIs. @@ -21,4 +29,3 @@ For some commands, the response messages may be slightly different because of th Removing a domain was a potentially troublesome operation: if the removal was to fail midway through the process, retrials wouldn't be accepted. This is fixed now, by first disabling and marking a domain for removal, then running all the handlers, and only on full success will the domain be removed. So if any failure is notified, the whole operation can be retried again. The database requires a migration, as the status of a domain takes now more than the two values a boolean allows, see the migrations for Postgres, MySQL and MSSQL in the [`priv/migrations`](https://github.com/esl/MongooseIM/tree/master/priv/migrations) directory. - diff --git a/doc/modules/mod_commands.md b/doc/modules/mod_commands.md deleted file mode 100644 index 86a28b5f0c2..00000000000 --- a/doc/modules/mod_commands.md +++ /dev/null @@ -1,79 +0,0 @@ -# MongooseIM's command set - -## Purpose - -This is a basic set of administration and client commands. -Our goal is to provide a consistent, easy to use API for MongooseIM. -Both backend and client commands provide enough information to allow auto-generating access methods. -We currently use it in our admin and client REST API interface. - -## Configuration - -This module contains command definitions loaded when the module is activated. -There are no more configuration parameters, so the following entry in the config file is sufficient: - -```toml -[modules.mod_commands] -``` - -## Command definition - -The module contains a list of command definitions. -Each definition contains the following entries: - -* name (uniquely identifies the command) -* category (used for listing commands and for generating URLs for REST API) -* subcategory (optional) -* desc (a brief description) -* module, function (what is called when the command is executed) -* action (create|read|update|delete) -* optional: security_policy (info to be used by the caller) -* args (a list of two-element tuples specifying name and type of an argument) -* result (what the command (and its underlying function) is supposed to return) - -A simple command definition may look like this: - -```erlang -[ - {name, list_contacts}, - {category, <<"contacts">>}, - {desc, <<"Get roster">>}, - {module, ?MODULE}, - {function, list_contacts}, - {action, read}, - {security_policy, [user]}, - {args, [{caller, binary}]}, - {result, []} -] -``` - -## Command registration and interface - -Command registry is managed by `mongoose_commands` module. -To register a command simply call: - -```erlang -mongoose_commands:register(list_of_command_definitions) -``` - -The registry provides functions for listing commands, retrieving their signatures, -and also calling. To call the above method you should do: - -```erlang -mongoose_commands:execute(admin, list_contacts) % if you want superuser privileges -``` - -or - -```erlang -mongoose_commands:execute(<<"alice@wonderland.lit">>, list_contacts) -``` - -and it will return a list of JIDs. REST API would expose this command as - -``` -http://localhost/api/contacts % use GET, since it is 'read' -``` - -and return a JSON list of strings. Since this is a user command, REST would expose it on the "client" -interface and require authorisation headers. diff --git a/doc/modules/mod_inbox.md b/doc/modules/mod_inbox.md index 45670a7fd1d..1ad70000e46 100644 --- a/doc/modules/mod_inbox.md +++ b/doc/modules/mod_inbox.md @@ -32,7 +32,7 @@ A list of supported inbox boxes by the server. This can be used by clients to cl !!! note `inbox`, `archive`, and `bin` are reserved box names and are always enabled, therefore they don't need to –and must not– be specified in this section. `all` has a special meaning in the box query and therefore is also not allowed as a box name. - If the asynchronous backend is configured, automatic removals become moves to the `bin` box, also called "Trash bin". This is to ensure eventual consistency. Then the bin can be emptied, either on a [user request](../open-extensions/inbox.md#examples-emptying-the-trash-bin), or through an [admin API endpoint](../mod_inbox_commands#admin-endpoint). + If the asynchronous backend is configured, automatic removals become moves to the `bin` box, also called "Trash bin". This is to ensure eventual consistency. Then the bin can be emptied, either on a [user request](../open-extensions/inbox.md#examples-emptying-the-trash-bin), with the `mongooseimctl inbox` command, through the [GraphQL API](../../graphql-api/Admin-GraphQL), or through the [REST API](../../rest-api/Administration-backend). #### `modules.mod_inbox.bin_ttl` * **Syntax:** non-negative integer, expressed in days. diff --git a/doc/modules/mod_inbox_commands.md b/doc/modules/mod_inbox_commands.md deleted file mode 100644 index 18713594bd9..00000000000 --- a/doc/modules/mod_inbox_commands.md +++ /dev/null @@ -1,47 +0,0 @@ -## Configuration -This module contains command definitions which are loaded when the module is activated. -There are no options to be provided, therefore the following entry in the config file is sufficient: - -```toml -[modules.mod_inbox_commands] -``` - -## Admin endpoint - -### Bin flush for a user -To clean the bin for a given user, the following admin API request can be triggered: - -```http -DELETE /api/inbox////bin, -``` -where `` and `` are the domain and name parts of the user's jid, respectively, and `` is the required number of days for an entry to be considered old enough to be removed, zero allowed (which clears all). - -The result would be a `200` with the number of rows that were removed as the body, or a corresponding error. For example, if only one entry was cleaned: -```http -HTTP/1.1 200 OK -server: Cowboy, -date: Wed, 30 Mar 2022 14:06:20 GMT, -content-type: application/json, -content-length: 1 - -1 -``` - -### Global bin flush -If all the bins were desired to be cleared, the following API can be used instead: - -```http -DELETE /api/inbox///bin, -``` -where as before, `` is the required number of days for an entry to be considered old enough to be removed, and `` is the host type where inbox is configured. - -The result would look analogously: -```http -HTTP/1.1 200 OK -server: Cowboy, -date: Wed, 30 Mar 2022 14:06:20 GMT, -content-type: application/json, -content-length: 1 - -42 -``` diff --git a/doc/modules/mod_muc_commands.md b/doc/modules/mod_muc_commands.md deleted file mode 100644 index 4ef1778df2c..00000000000 --- a/doc/modules/mod_muc_commands.md +++ /dev/null @@ -1,65 +0,0 @@ -# MongooseIM's multi-user chat commands set - -## Purpose -This is a set of commands, providing actions related to multi-user chat features. - -## Configuration -This module contains command definitions which are loaded when the module is activated. -There are no options to be provided, therefore the following entry in the config file is sufficient: - -```toml -[modules.mod_muc_commands] -``` - -## Commands - -This file consists of [commands definitions](mod_commands.md). -Following commands (along with functions necessary for them to run) are defined: - -### `create_muc_room` - -Creates a MUC room. - -Arguments: - -* `host` (binary) -* `name` (binary) - room name -* `owner` (binary) - the XMPP entity that would normally request an instant MUC room -* `nick` (binary) - -### `kick_user_from_room` - -Kicks a user from a MUC room (on behalf of a moderator). - -Arguments: - -* `host` (binary) -* `name` (binary) -* `nick` (binary) - -### `invite_to_muc_room` - -Sends a MUC room invite (direct) from one user to another. - -Arguments: - -* `host` (binary) -* `name` (binary) -* `sender` (binary) -* `recipient` (binary) -* `reason` (binary) - -### `send_message_to_room` - -Sends a message to a MUC room from a given room. - -Arguments: - -* `host` (binary) -* `name` (binary) -* `from` (binary) -* `body` (binary) - -## Running commands - -Commands must be [registered and then run](mod_commands.md) using the module `mongoose_commands`. diff --git a/doc/modules/mod_muc_light_commands.md b/doc/modules/mod_muc_light_commands.md deleted file mode 100644 index 68eb985b52a..00000000000 --- a/doc/modules/mod_muc_light_commands.md +++ /dev/null @@ -1,69 +0,0 @@ -# MongooseIM's multi-user chat light commands set - -## Purpose - -This is a set of commands, providing actions related to multi-user chat light features. -These commands are used by REST API modules. - -## Configuration - -This module contains command definitions which are loaded when the module is activated. -There are no options to be provided, therefore the following entry in the config file is sufficient: - -```toml -[modules.mod_muc_light_commands] -``` - -## Commands - -This file consists of [commands definitions](mod_commands.md). -Following commands (along with functions necessary for them to run) are defined: - -### `create_muc_light_room` - -Create a MUC Light room with unique username part in JID. - -Arguments: - -* `domain` (binary) -* `name` (binary) -* `owner` (binary) -* `subject` (binary) - -### `create_identifiable_muc_light_room` - -Creates a MUC Light room with user-provided username part in JID. - -Arguments: - -* `domain` (binary) -* `id` (binary) -* `name` (binary) -* `owner` (binary) -* `subject` (binary) - -### `invite_to_room` - -Invites to a MUC Light room. - -Arguments: - -* `domain` (binary) -* `name` (binary) -* `sender` (binary) -* `recipient` (binary) - -### `send_message_to_muc_light_room` - -Sends a message to a MUC Light room. - -Arguments: - -* `domain` (binary) -* `name` (binary) -* `from` (binary) -* `body` (binary) - -## Running commands - -Commands must be [registered and then run](mod_commands.md) using the module `mongoose_commands`. diff --git a/doc/rest-api/Administration-backend.md b/doc/rest-api/Administration-backend.md index 377972bd408..ac643f6f610 100644 --- a/doc/rest-api/Administration-backend.md +++ b/doc/rest-api/Administration-backend.md @@ -2,83 +2,13 @@ ## Configuration -Commands used by the REST API are provided by modules: - -`mod_commands` - provides general purpose commands: both user-like (e.g. sending a message and retrieving messages from the archive) and administration-like (e.g. create/delete a user and change the password). - -`mod_muc_commands` - commands related to Multi-user Chat rooms: create a room, invite users, send a message etc. - -`mod_muc_light_commands` - same but for rooms based on the muc-light protocol. - -To activate those commands, put the modules you need into the `mongooseim.toml` file: - -```toml - [modules.mod_commands] - - [modules.mod_muc_commands] - - [modules.mod_muc_light_commands] - -``` - -You also have to hook the `mongoose_api_admin` module to an HTTP endpoint as described -in the [admin REST API handlers configuration](../configuration/listen.md#handler-types-rest-api-admin-mongoose_api_admin) +To enable the commands, you need to hook the `mongoose_admin_api` module to an HTTP endpoint as described +in the [admin REST API handlers configuration](../configuration/listen.md#handler-types-rest-api-admin-mongoose_admin_api) section of the [listeners](../configuration/listen.md) documentation. -## Listing commands - -To get a list of commands, you can use `/api/commands` endpoint. -Use `jq` utility for pretty-printing JSON. - -Each command has the fields: - -- `path` - URL path for this command -- `method` - HTTP method to use for this command -- `args` - arguments to provide inside a path or as POST arguments -- `category` - a name used for grouping similar commands -- `name` - a command name -- `desc` - description text -- `action` - a type of a command, corresponding to `method` - -`path`, `method` and `args` are useful to figuring out how the request should -look like (you can use Swagger instead). - -`name`, `desc` and `category` are used just as metadata (they are not part of -any request). - -`action` is used internally. - -```json -curl -v "http://localhost:8088/api/commands" | jq -[ - { - "path": "/commands", - "name": "list_methods", - "method": "GET", - "desc": "List commands", - "category": "commands", - "args": {}, - "action": "read" - }, - { - "path": "/contacts", - "name": "add_contact", - "method": "POST", - "desc": "Add a contact to roster", - "category": "contacts", - "args": { - "jid": "binary", - "caller": "binary" - }, - "action": "create" - }, -... -``` - - ## OpenAPI specifications -Read the beautiful [Swagger documentation](https://esl.github.io/MongooseDocs/latest/swagger/index.html) for more information. +Read the [Swagger documentation](https://esl.github.io/MongooseDocs/latest/swagger/index.html) for more information. [![Swagger](https://nordicapis.com/wp-content/uploads/swagger-Top-Specification-Formats-for-REST-APIs-nordic-apis-sandoval-e1441412425742-300x170.png)](https://esl.github.io/MongooseDocs/latest/swagger/index.html) diff --git a/doc/rest-api/Administration-backend_swagger.yml b/doc/rest-api/Administration-backend_swagger.yml index 4517d3bc79d..1120dc7fc11 100644 --- a/doc/rest-api/Administration-backend_swagger.yml +++ b/doc/rest-api/Administration-backend_swagger.yml @@ -11,6 +11,9 @@ info: Please note that many of the fields such as **username** or **caller** expect a **JID** (jabber identifier, f.e. **alice@wonderland.com**). There are two types of **JIDs**: * **bare JID** - consists of **username** and **domain name** (XMPP host, usually the one set in your `mongooseim.toml` file). * **full JID** - is a **bare JID** with online user's resource to uniquely identify user's connection (f.e. **alice@wonderland.com/resource**). + + You should enable authentication to make sure the server can identify who sent the request and if it comes from an authorized user. + Currently the only supported method is **Basic Auth**. schemes: - http basePath: /api @@ -20,29 +23,6 @@ produces: - application/json host: "localhost:8088" paths: - /commands: - get: - description: Lists the available commands for administering MongooseIM. - tags: - - "Commands" - responses: - 200: - description: A list of information on all the commands that are currently available. - schema: - title: commandList - type: array - items: - title: commandDescription - type: object - properties: - name: - type: string - category: - type: string - action: - type: string - desc: - type: string /users/{XMPPHost}: parameters: - $ref: '#/parameters/hostName' @@ -661,6 +641,293 @@ paths: responses: 204: description: User was kicked out from the MUC room. + /inbox/{domain}/{userName}/{days}/bin: + parameters: + - name: domain + in: path + description: Domain part of the user's JID + required: true + type: string + - name: userName + in: path + description: Name part of the user's JID + required: true + type: string + - $ref: '#/parameters/days' + delete: + tags: + - "Inbox management" + description: Clean the bin for a given user + responses: + 200: + description: The bin has been cleaned. The number of rows removed is returned as the body. + /inbox/{hostType}/{days}/bin: + parameters: + - $ref: '#/parameters/hostType' + - $ref: '#/parameters/days' + delete: + tags: + - "Inbox management" + description: Clean the bins of all users from a given host type + responses: + 200: + description: The bin has been cleaned. The number of rows removed is returned as the body. + /domains/{domain}: + put: + description: Adds a domain. + tags: + - "Dynamic domains" + parameters: + - in: path + name: domain + required: true + type: string + - in: body + name: host_type + description: The host type of the domain. + required: true + schema: + title: host_type + type: object + properties: + host_type: + example: "type1" + type: string + responses: + 204: + description: Domain was successfully inserted. + 400: + description: Bad request. + 409: + description: Domain already exists with a different host type. + 403: + description: DB service disabled, or the host type is unknown. + 500: + description: Other errors. + patch: + description: Enables/disables a domain. + tags: + - "Dynamic domains" + parameters: + - in: path + name: domain + required: true + type: string + - in: body + name: enabled + description: Whether to enable or to disable a domain. + required: true + schema: + title: Enabled + type: object + properties: + enabled: + example: true + type: boolean + responses: + 204: + description: Domain was successfully updated. + 404: + description: Domain not found. + 403: + description: Domain is static, or the service is disabled. + 500: + description: Other errors. + get: + description: Returns information about the domain. + tags: + - "Dynamic domains" + parameters: + - name: domain + type: string + in: path + required: true + responses: + 200: + description: Successful response. + 404: + description: Domain not found. + delete: + description: "Removes a domain" + tags: + - "Dynamic domains" + parameters: + - in: path + name: domain + required: true + type: string + - in: body + name: host_type + description: The host type of the domain. + required: true + schema: + title: host_type + type: object + properties: + host_type: + example: "type1" + type: string + responses: + 204: + description: "The domain is removed or not found." + 403: + description: | + One of: + * the domain is static. + * the DB service is disabled. + * the host type is wrong (does not match the host type in the database). + * the host type is unknown. + 500: + description: "Other errors." + /metrics/: + get: + description: Returns a list of host type names and metric names + tags: + - "Metrics" + responses: + 200: + description: Host type names and metric names. + schema: + type: object + properties: + host_types: + schema: + type: array + items: + type: string + example: + - "localhost" + metrics: + schema: + type: array + items: + type: string + example: + - "xmppErrorIq" + - "xmppPresenceReceived" + - "xmppMessageBounced" + global: + schema: + type: array + items: + type: string + example: + - "nodeSessionCount" + - "totalSessionCount" + - "uniqueSessionCount" + /metrics/all: + get: + description: Returns a list of metrics aggregated for all host types + tags: + - "Metrics" + responses: + 200: + description: Metrics + schema: + type: object + properties: + metrics: + type: object + example: + modRosterPush: + one: 0 + count: 0 + /metrics/all/{metric}: + parameters: + - $ref: '#/parameters/metric' + get: + description: Returns the metric value aggregated for all host types + tags: + - "Metrics" + responses: + 200: + description: Aggregated metric value + schema: + type: object + properties: + metric: + type: object + example: + one: 0 + count: 0 + 404: + description: There is no such metric + /metrics/host_type/{hostType}: + parameters: + - $ref: '#/parameters/hostType' + get: + description: Returns the values of all host-type metrics + tags: + - "Metrics" + responses: + 200: + description: Metrics + schema: + type: object + properties: + metrics: + type: object + example: + modRosterPush: + one: 0 + count: 0 + 404: + description: There is no such host type + /metrics/host_type/{hostType}/{metric}: + parameters: + - $ref: '#/parameters/hostType' + - $ref: '#/parameters/metric' + get: + description: Returns the value of a host-type metric + tags: + - "Metrics" + responses: + 200: + description: Metric value + schema: + type: object + properties: + metric: + type: object + example: + one: 0 + count: 0 + 404: + description: There is no such metric + /metrics/global: + get: + description: Returns the values of all global metrics + tags: + - "Metrics" + responses: + 200: + description: Metrics + schema: + type: object + properties: + type: object + example: + nodeUpTime: + value: 6604 + /metrics/global/{metric}: + parameters: + - $ref: '#/parameters/metric' + get: + description: Returns the value of a global metric + tags: + - "Metrics" + responses: + 200: + description: Metric value + schema: + type: object + properties: + metric: + type: object + example: + value: 6604 + 404: + description: There is no such global metric parameters: MUCServer: @@ -676,7 +943,12 @@ parameters: description: The XMPP host served by the server. required: true type: string - format: hostname + hostType: + name: hostType + in: path + description: Host type configured on the server + required: true + type: string roomName: name: roomName in: path @@ -689,6 +961,18 @@ parameters: description: The MUC Light room's **id** required: true type: string + days: + name: days + in: path + description: Number of days for an entry to be considered old enough to be removed, zero allowed (which clears all) + required: true + type: integer + metric: + name: metric + description: Metric name + in: path + required: true + type: string definitions: messageList: diff --git a/doc/rest-api/Client-frontend.md b/doc/rest-api/Client-frontend.md index 4173c7c2929..12238c84d38 100644 --- a/doc/rest-api/Client-frontend.md +++ b/doc/rest-api/Client-frontend.md @@ -13,7 +13,7 @@ Please see the [Authentication](#authentication) section for more details. 1. The relevant endpoint has to be configured on the server side. See the [configuration section](#configuration). 1. A list of provided actions is documented with Swagger. -See the beautiful [specification](https://esl.github.io/MongooseDocs/latest/swagger/index.html?client=true). +See the [specification](https://esl.github.io/MongooseDocs/latest/swagger/index.html?client=true). ## Authentication @@ -36,7 +36,7 @@ Authorization: Basic YWxpY2VAbG9jYWxob3N0OnNlY3JldA== ## Configuration -Handlers have to be configured as shown in the [REST API configuration example](../configuration/listen.md#example-3-client-api) +Handlers have to be configured as shown in the [REST API configuration example](../configuration/listen.md#example-6-client-rest-api) to enable REST API. In order to get the client REST API up and running simply copy the provided example. @@ -85,7 +85,7 @@ then in the final json message these properties will be converted to json map wi ## OpenAPI specifications -See the beautiful [Swagger documentation](https://esl.github.io/MongooseDocs/latest/swagger/index.html?client=true) for more information. +See the [Swagger documentation](https://esl.github.io/MongooseDocs/latest/swagger/index.html?client=true) for more information. [![Swagger](https://nordicapis.com/wp-content/uploads/swagger-Top-Specification-Formats-for-REST-APIs-nordic-apis-sandoval-e1441412425742-300x170.png)](https://esl.github.io/MongooseDocs/latest/swagger/index.html?client=true) diff --git a/doc/rest-api/Dynamic-domains.md b/doc/rest-api/Dynamic-domains.md deleted file mode 100644 index c3f52153a51..00000000000 --- a/doc/rest-api/Dynamic-domains.md +++ /dev/null @@ -1,37 +0,0 @@ -# MongooseIM's REST API for dynamic domain management - -Provides API for adding/removing and enabling/disabling domains over HTTP. -Implemented by `mongoose_domain_handler` module. - -## Configuration - -`mongoose_domain_handler` has to be configured as shown in the [REST API configuration example](../configuration/listen.md#example-4-domain-api) -to enable the REST API. - -For details about possible configuration parameters please see the relevant -documentation of the [listeners](../configuration/listen.md), -in particular the [`mongoose_domain_handler`](../configuration/listen.md#handler-types-rest-api---domain-management---mongoose_domain_handler) -section. - -## OpenAPI specifications - -Read our [Swagger documentation](https://esl.github.io/MongooseDocs/latest/swagger/index.html?domains=true) for more information. - -[![Swagger](https://nordicapis.com/wp-content/uploads/swagger-Top-Specification-Formats-for-REST-APIs-nordic-apis-sandoval-e1441412425742-300x170.png)](https://esl.github.io/MongooseDocs/latest/swagger/index.html?domains=true) - - - - diff --git a/doc/rest-api/Dynamic-domains_swagger.yml b/doc/rest-api/Dynamic-domains_swagger.yml deleted file mode 100644 index 887ee848d01..00000000000 --- a/doc/rest-api/Dynamic-domains_swagger.yml +++ /dev/null @@ -1,146 +0,0 @@ -swagger: '2.0' -info: - version: "1.0.0" - title: "MongooseIM's domain management REST API" - description: | - Explore MongooseIM features using our REST API. - - Please keep in mind that all requests require authentication. This is to make sure the server can identify who sent the request and if it comes from an authorized user. - Currently the only supported method is **Basic Auth**. - -basePath: /api -schemes: - - http -produces: - - application/json -consumes: - - application/json -host: "localhost:8088" -paths: - /domains/{domain}: - put: - description: Adds a domain. - tags: - - "Dynamic domains" - parameters: - - in: path - name: domain - required: true - type: string - - in: body - name: host_type - description: The host type of the domain. - required: true - schema: - title: host_type - type: object - properties: - host_type: - example: "type1" - type: string - responses: - 204: - description: Domain was successfully inserted. - 400: - description: Bad request. - examples: - application/json: {"what": "body is empty"} - 409: - description: Domain already exists with a different host type. - examples: - application/json: {"what": "duplicate"} - 403: - description: DB service disabled, or the host type is unknown. - examples: - application/json: {"what": "domain is static"} - 500: - description: Other errors. - examples: - application/json: {"what": "database error"} - patch: - description: Enables/disables a domain. - tags: - - "Dynamic domains" - parameters: - - in: path - name: domain - required: true - type: string - - in: body - name: enabled - description: Whether to enable or to disable a domain. - required: true - schema: - title: Enabled - type: object - properties: - enabled: - example: true - type: boolean - responses: - 204: - description: Domain was successfully updated. - 404: - description: Domain not found. - examples: - application/json: {"what": "domain not found"} - 403: - description: Domain is static, or the service is disabled. - examples: - application/json: {"what": "domain is static"} - 500: - description: Other errors. - examples: - application/json: {"what": "database error"} - get: - description: Returns information about the domain. - tags: - - "Dynamic domains" - parameters: - - name: domain - type: string - in: path - required: true - responses: - 200: - description: Successful response. - 404: - description: Domain not found. - examples: - application/json: {"what": "domain not found"} - delete: - description: "Removes a domain" - tags: - - "Dynamic domains" - parameters: - - in: path - name: domain - required: true - type: string - - in: body - name: host_type - description: The host type of the domain. - required: true - schema: - title: host_type - type: object - properties: - host_type: - example: "type1" - type: string - responses: - 204: - description: "The domain is removed or not found." - 403: - description: | - One of: - * the domain is static. - * the DB service is disabled. - * the host type is wrong (does not match the host type in the database). - * the host type is unknown. - examples: - application/json: {"what": "unknown host type"} - 500: - description: "Other errors." - examples: - application/json: {"what": "database error"} diff --git a/doc/rest-api/Metrics-backend.md b/doc/rest-api/Metrics-backend.md deleted file mode 100644 index 57c58b9d618..00000000000 --- a/doc/rest-api/Metrics-backend.md +++ /dev/null @@ -1,136 +0,0 @@ -## Introduction - -!!! Warning - This API is considered obsolete. - Please use [WombatOAM](https://www.erlang-solutions.com/capabilities/wombatoam/) for monitoring or one of the [exometer reporters](../operation-and-maintenance/Logging-&-monitoring.md#monitoring) and your favourite statistics service. - -To expose MongooseIM metrics, an adequate endpoint must be included in the [listen](../configuration/listen.md) -section of `mongooseim.toml`. The specific configuration options are described in -the [metrics API handlers](../configuration/listen.md#handler-types-metrics-api-obsolete-mongoose_api) -section. - -An example configuration: - -```toml -[[listen.http]] - port = 5288 - transport.num_acceptors = 5 - transport.max_connections = 10 - - [[listen.http.handlers.mongoose_api]] - host = "localhost" - path = "/api" - handlers = ["mongoose_api_metrics"] -``` - -If you'd like to learn more about metrics in MongooseIM, please visit [MongooseIM metrics](../operation-and-maintenance/MongooseIM-metrics.md) page. - -### Security notice - -An auth mechanism is available only for the new administration API. -That's why we recommend to expose this API only using a private interface or a port hidden behind a firewall to limit the access to the API. -The above configuration starts the API only on a loopback interface. - -## Response format - -The responses are composed in a JSON format with a root element containing one or more attributes as response elements. - -Example response: -```json -{ - "hosts": [ - "localhost" - ], - "metrics": [ - "xmppErrorIq", - "xmppPresenceReceived", - "xmppMessageBounced", - (...) - ], - "global": [ - "nodeSessionCount", - "totalSessionCount", - "uniqueSessionCount", - (...) - ] -} -``` - -## Services - -### GET /api/metrics - -Returns ```200 OK``` and two elements: - -* `host_types` - A list of host type names available on the server. -* `metrics` - A list of per-host metrics. -* `global` - A list of global metrics. - -### GET /api/metrics/all - -Returns ```200 OK``` and an element: - -* `metrics` - A list of aggregated (sum of all domains) per host type metrics with their values. - -### GET /api/metrics/all/:metric - -On success returns ```200 OK``` and an element: - -* `metric` - An aggregated (sum of all domains) per host type metric. - -Returns ```404 Not Found``` when metric `:metric` doesn't exist. - -### GET /api/metrics/host_type/:host_type - -On success returns ```200 OK``` and an element: - -* `metrics` - A list of per host type metrics and their values for host_type `:host_type`. - -Returns ```404 Not Found``` when host_type `:host_type` doesn't exist. - -### GET /api/metrics/host_type/:host_type/:metric - -On success returns ```200 OK``` and an element: - -* `metric` - A per host type metric `:metric` and its value for host_type `:host_type`. - -Returns ```404 Not Found``` when the pair (host_type `:host_type`, metric `:metric`) doesn't exist. - -### GET /api/metrics/global - -On success returns ```200 OK``` and an element: - -* `metrics` - A list of all global metrics and their values. - -### GET /api/metrics/global/:metric - -On success returns ```200 OK``` and an element: - -* `metric` - A global metric `:metric` and its value. - -Returns ```404 Not Found``` when metric `:metric` doesn't exist. - -## collectd integration - -The interface is compatible with the collectd curl_json plugin. -Data fetched by collectd may be later visualized by tools like Graphite. - -Here's an example of a collectd configuration entry that will fetch all available metrics for a given host: -```json -LoadPlugin curl_json -... - - :/api/metrics/host/"> - Instance "mongooseim" - - Type "absolute" - - - Type "absolute" - - - Type "absolute" - - - -``` diff --git a/doc/rest-api/Metrics-backend_swagger.yml b/doc/rest-api/Metrics-backend_swagger.yml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/doc/swagger/index.html b/doc/swagger/index.html index 13594fbbeba..fdb2f5f5786 100644 --- a/doc/swagger/index.html +++ b/doc/swagger/index.html @@ -46,9 +46,6 @@ if (searchParams.has('client')) { return "Client-frontend_swagger.yml" } - if (searchParams.has('domains')) { - return "Dynamic-domains_swagger.yml" - } return "Administration-backend_swagger.yml" } diff --git a/doc/user-guide/Features.md b/doc/user-guide/Features.md index e57eef6ad6e..1b967e3d1d6 100644 --- a/doc/user-guide/Features.md +++ b/doc/user-guide/Features.md @@ -70,7 +70,7 @@ For load testing we use [our own tools](../Contributions.md#amoc), that enable u MongooseIM supports multi-tenancy. This makes it possible to set up thousands of domains dynamically without a noticeable performance overhead. -On more information on how to set up this feature, see [dynamic domains configuration](../configuration/general.md#generalhost_types) and [REST API for dynamic domains](../rest-api/Dynamic-domains.md). +On more information on how to set up this feature, see [dynamic domains configuration](../configuration/general.md#generalhost_types). ## Integration with other platform components diff --git a/mkdocs.yml b/mkdocs.yml index 73beca05ad7..dc30d25008d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,7 +99,6 @@ nav: - 'mod_caps': 'modules/mod_caps.md' - 'mod_cache_users': 'modules/mod_cache_users.md' - 'mod_carboncopy': 'modules/mod_carboncopy.md' - - 'mod_commands': 'modules/mod_commands.md' - 'mod_csi': 'modules/mod_csi.md' - 'mod_disco': 'modules/mod_disco.md' - 'mod_domain_isolation': 'modules/mod_domain_isolation.md' @@ -113,16 +112,13 @@ nav: - 'mod_global_distrib': 'modules/mod_global_distrib.md' - 'mod_http_upload': 'modules/mod_http_upload.md' - 'mod_inbox': 'modules/mod_inbox.md' - - 'mod_inbox_commands': 'modules/mod_inbox_commands.md' - 'mod_jingle_sip': 'modules/mod_jingle_sip.md' - 'mod_keystore': 'modules/mod_keystore.md' - 'mod_last': 'modules/mod_last.md' - 'mod_mam': 'modules/mod_mam.md' - 'mod_muc': 'modules/mod_muc.md' - - 'mod_muc_commands': 'modules/mod_muc_commands.md' - 'mod_muc_log': 'modules/mod_muc_log.md' - 'mod_muc_light': 'modules/mod_muc_light.md' - - 'mod_muc_light_commands': 'modules/mod_muc_light_commands.md' - 'mod_offline': 'modules/mod_offline.md' - 'mod_offline_stub': 'modules/mod_offline_stub.md' - 'mod_ping': 'modules/mod_ping.md' @@ -141,9 +137,7 @@ nav: - 'mod_version': 'modules/mod_version.md' - 'REST API': - 'Client/frontend': 'rest-api/Client-frontend.md' - - 'Metrics backend': 'rest-api/Metrics-backend.md' - 'Administration backend': 'rest-api/Administration-backend.md' - - 'Dynamic domains': 'rest-api/Dynamic-domains.md' - 'GraphQL API': - 'User': 'graphql-api/User-GraphQL.md' - 'Admin': 'graphql-api/Admin-GraphQL.md' diff --git a/src/mongoose_admin_api/mongoose_admin_api.erl b/src/mongoose_admin_api/mongoose_admin_api.erl index 4a0f61ed073..1ec7b90f146 100644 --- a/src/mongoose_admin_api/mongoose_admin_api.erl +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -158,7 +158,7 @@ error_response(ErrorType, Message, Req, State) -> error_type => ErrorType, message => BinMessage, req => Req}), - Req1 = cowboy_req:reply(error_code(ErrorType), #{}, BinMessage, Req), + Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req), {stop, Req1, State}. -spec error_code(error_type()) -> non_neg_integer(). From d7a4438033bd02ae844dacec37b2ceb306462b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Mon, 3 Oct 2022 10:20:43 +0200 Subject: [PATCH 096/117] Refactored hook handler in mod_global_distrib module --- src/global_distrib/mod_global_distrib.erl | 31 +++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/global_distrib/mod_global_distrib.erl b/src/global_distrib/mod_global_distrib.erl index 1cd5c60f4b2..f7f0f4f5a4e 100644 --- a/src/global_distrib/mod_global_distrib.erl +++ b/src/global_distrib/mod_global_distrib.erl @@ -28,10 +28,10 @@ -export([deps/2, start/2, stop/1, config_spec/0]). -export([find_metadata/2, get_metadata/3, remove_metadata/2, put_metadata/3]). --export([maybe_reroute/1]). +-export([maybe_reroute/3]). -export([process_opts/1, process_endpoint/1]). --ignore_xref([maybe_reroute/1, remove_metadata/2]). +-ignore_xref([remove_metadata/2]). %%-------------------------------------------------------------------- %% gen_mod API @@ -59,19 +59,19 @@ bounce_modules(#{enabled := false}) -> []. start(HostType, #{global_host := HostType}) -> mongoose_metrics:ensure_metric(global, ?GLOBAL_DISTRIB_DELIVERED_WITH_TTL, histogram), mongoose_metrics:ensure_metric(global, ?GLOBAL_DISTRIB_STOP_TTL_ZERO, spiral), - ejabberd_hooks:add(hooks()); + gen_hook:add_handlers(hooks()); start(_HostType, #{}) -> ok. -spec stop(mongooseim:host_type()) -> any(). stop(HostType) -> case gen_mod:get_module_opt(HostType, ?MODULE, global_host) of - HostType -> ejabberd_hooks:delete(hooks()); + HostType -> gen_hook:delete_handlers(hooks()); _ -> ok end. hooks() -> - [{filter_packet, global, ?MODULE, maybe_reroute, 99}]. + [{filter_packet, global, fun ?MODULE:maybe_reroute/3, #{}, 99}]. -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> @@ -224,13 +224,15 @@ remove_metadata(Acc, Key) -> %% Hooks implementation %%-------------------------------------------------------------------- --spec maybe_reroute(drop) -> drop; - ({jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}) -> - drop | {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}. -maybe_reroute(drop) -> drop; +-spec maybe_reroute(drop, _, _) -> drop; + (FPacket, Params, Extra) -> {ok, drop} | {ok, FPacket} when + FPacket :: {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}, + Params :: map(), + Extra :: map(). +maybe_reroute(drop, _, _) -> {ok, drop}; maybe_reroute({#jid{ luser = SameUser, lserver = SameServer } = _From, #jid{ luser = SameUser, lserver = SameServer } = _To, - _Acc, _Packet} = FPacket) -> + _Acc, _Packet} = FPacket, _, _) -> %% GD is not designed to support two user sessions existing in distinct clusters %% and here we explicitly block routing stanzas between them. %% Without this clause, test_pm_with_ungraceful_reconnection_to_different_server test @@ -238,8 +240,8 @@ maybe_reroute({#jid{ luser = SameUser, lserver = SameServer } = _From, %% was poisoning reg1 cache. In such case, reg1 tried to route locally stanzas %% from unacked SM buffer, leading to an error, while a brand new, shiny Eve %% on mim1 was waiting. - FPacket; -maybe_reroute({From, To, _, Packet} = FPacket) -> + {ok, FPacket}; +maybe_reroute({From, To, _, Packet} = FPacket, _, _) -> Acc = maybe_initialize_metadata(FPacket), {ok, ID} = find_metadata(Acc, id), LocalHost = opt(local_host), @@ -247,7 +249,7 @@ maybe_reroute({From, To, _, Packet} = FPacket) -> %% If target_host_override is set (typically when routed out of bounce storage), %% host lookup is skipped and messages are routed to target_host_override value. TargetHostOverride = get_metadata(Acc, target_host_override, undefined), - case lookup_recipients_host(TargetHostOverride, To, LocalHost, GlobalHost) of + ResultFPacket = case lookup_recipients_host(TargetHostOverride, To, LocalHost, GlobalHost) of {ok, LocalHost} -> %% Continue routing with initialized metadata mongoose_hooks:mod_global_distrib_known_recipient(GlobalHost, @@ -288,7 +290,8 @@ maybe_reroute({From, To, _, Packet} = FPacket) -> ?LOG_DEBUG(#{what => gd_route_failed, gd_id => ID, acc => Acc, text => <<"Unable to route global: user not found in the routing table">>}), mongoose_hooks:mod_global_distrib_unknown_recipient(GlobalHost, {From, To, Acc, Packet}) - end. + end, + {ok, ResultFPacket}. %%-------------------------------------------------------------------- %% Helpers From 54a1d188184e8da21469c72a3d109e36b0f03935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Tue, 4 Oct 2022 13:34:55 +0200 Subject: [PATCH 097/117] Refactored hook handlers in mod_global_distrib_bounce module --- src/global_distrib/mod_global_distrib.erl | 4 +- .../mod_global_distrib_bounce.erl | 43 ++++++++++--------- src/mongoose_hooks.erl | 9 +++- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/global_distrib/mod_global_distrib.erl b/src/global_distrib/mod_global_distrib.erl index f7f0f4f5a4e..0c188baa751 100644 --- a/src/global_distrib/mod_global_distrib.erl +++ b/src/global_distrib/mod_global_distrib.erl @@ -224,9 +224,9 @@ remove_metadata(Acc, Key) -> %% Hooks implementation %%-------------------------------------------------------------------- --spec maybe_reroute(drop, _, _) -> drop; +-spec maybe_reroute(drop, _, _) -> {ok, drop}; (FPacket, Params, Extra) -> {ok, drop} | {ok, FPacket} when - FPacket :: {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}, + FPacket :: {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}, Params :: map(), Extra :: map(). maybe_reroute(drop, _, _) -> {ok, drop}; diff --git a/src/global_distrib/mod_global_distrib_bounce.erl b/src/global_distrib/mod_global_distrib_bounce.erl index c638c07f9bb..7a65a4c89a1 100644 --- a/src/global_distrib/mod_global_distrib_bounce.erl +++ b/src/global_distrib/mod_global_distrib_bounce.erl @@ -30,10 +30,10 @@ -export([start_link/0, start/2, stop/1, deps/2]). -export([init/1, handle_info/2, handle_cast/2, handle_call/3, code_change/3, terminate/2]). --export([maybe_store_message/1, reroute_messages/4]). +-export([maybe_store_message/3, reroute_messages/3]). -export([bounce_queue_size/0]). --ignore_xref([bounce_queue_size/0, maybe_store_message/1, reroute_messages/4, start_link/0]). +-ignore_xref([bounce_queue_size/0, start_link/0]). %%-------------------------------------------------------------------- %% gen_mod API @@ -46,14 +46,14 @@ start(HostType, _Opts) -> EvalDef = {[{l, [{t, [value, {v, 'Value'}]}]}], [value]}, QueueSizeDef = {function, ?MODULE, bounce_queue_size, [], eval, EvalDef}, mongoose_metrics:ensure_metric(global, ?GLOBAL_DISTRIB_BOUNCE_QUEUE_SIZE, QueueSizeDef), - ejabberd_hooks:add(hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), ChildSpec = {?MODULE, {?MODULE, start_link, []}, permanent, 1000, worker, [?MODULE]}, ejabberd_sup:start_child(ChildSpec). -spec stop(mongooseim:host_type()) -> any(). stop(HostType) -> ejabberd_sup:stop_child(?MODULE), - ejabberd_hooks:delete(hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)), ets:delete(?MS_BY_TARGET), ets:delete(?MESSAGE_STORE). @@ -62,8 +62,8 @@ deps(_HostType, Opts) -> [{mod_global_distrib_utils, Opts, hard}]. hooks(HostType) -> - [{mod_global_distrib_unknown_recipient, HostType, ?MODULE, maybe_store_message, 80}, - {mod_global_distrib_known_recipient, HostType, ?MODULE, reroute_messages, 80}]. + [{mod_global_distrib_unknown_recipient, HostType, fun ?MODULE:maybe_store_message/3, #{}, 80}, + {mod_global_distrib_known_recipient, HostType, fun ?MODULE:reroute_messages/3, #{}, 80}]. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> @@ -99,14 +99,16 @@ terminate(_Reason, _State) -> %% Hooks implementation %%-------------------------------------------------------------------- --spec maybe_store_message(drop) -> drop; - ({jid:jid(), jid:jid(), mongoose_acc:t(), exml:packet()}) -> - drop | {jid:jid(), jid:jid(), mongoose_acc:t(), exml:packet()}. -maybe_store_message(drop) -> drop; -maybe_store_message({From, To, Acc0, Packet} = FPacket) -> +-spec maybe_store_message(drop, _, _) -> {ok, drop}; + (FPacket, Params, Extra) -> {ok, drop} | {ok, FPacket} when + FPacket :: {jid:jid(), jid:jid(), mongoose_acc:t(), exml:element()}, + Params :: map(), + Extra :: map(). +maybe_store_message(drop, _, _) -> {ok, drop}; +maybe_store_message({From, To, Acc0, Packet} = FPacket, _, _) -> LocalHost = opt(local_host), {ok, ID} = mod_global_distrib:find_metadata(Acc0, id), - case mod_global_distrib:get_metadata(Acc0, {bounce_ttl, LocalHost}, + ResultAcc = case mod_global_distrib:get_metadata(Acc0, {bounce_ttl, LocalHost}, opt([bounce, max_retries])) of 0 -> ?LOG_DEBUG(#{what => gd_skip_store_message, @@ -128,13 +130,14 @@ maybe_store_message({From, To, Acc0, Packet} = FPacket) -> ResendAt = erlang:monotonic_time() + ResendAfter, do_insert_in_store(ResendAt, {From, To, Acc, Packet}), drop - end. - --spec reroute_messages(SomeAcc :: mongoose_acc:t(), - From :: jid:jid(), - To :: jid:jid(), - TargetHost :: binary()) -> mongoose_acc:t(). -reroute_messages(Acc, From, To, TargetHost) -> + end, + {ok, ResultAcc}. + +-spec reroute_messages(Acc, Params, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Params :: #{from := jid:jid(), to := jid:jid(), target_host := binary()}, + Extra :: map(). +reroute_messages(Acc, #{from := From, to := To, target_host := TargetHost}, _) -> Key = get_index_key(From, To), StoredMessages = lists:filtermap( @@ -150,7 +153,7 @@ reroute_messages(Acc, From, To, TargetHost) -> text => <<"Routing multiple previously stored messages">>, stored_messages_length => length(StoredMessages), acc => Acc}), lists:foreach(pa:bind(fun reroute_message/2, TargetHost), StoredMessages), - Acc. + {ok, Acc}. %%-------------------------------------------------------------------- %% API for metrics diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 006bb641aa6..abd4b2d4145 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -1515,8 +1515,11 @@ pubsub_publish_item(Server, NodeId, Publisher, ServiceJID, ItemId, BrPayload) -> LocalHost :: jid:server(), Result :: any(). mod_global_distrib_known_recipient(GlobalHost, From, To, LocalHost) -> + Params = #{from => From, to => To, target_host => LocalHost}, + Args = [From, To, LocalHost], + ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), run_hook_for_host_type(mod_global_distrib_known_recipient, GlobalHost, ok, - [From, To, LocalHost]). + ParamsWithLegacyArgs). %%% @doc The `mod_global_distrib_unknown_recipient' hook is called when %%% the recipient is unknown to `global_distrib'. @@ -1525,7 +1528,9 @@ mod_global_distrib_known_recipient(GlobalHost, From, To, LocalHost) -> Info :: filter_packet_acc(), Result :: any(). mod_global_distrib_unknown_recipient(GlobalHost, Info) -> - run_hook_for_host_type(mod_global_distrib_unknown_recipient, GlobalHost, Info, []). + ParamsWithLegacyArgs = ejabberd_hooks:add_args(#{}, []), + run_hook_for_host_type(mod_global_distrib_unknown_recipient, GlobalHost, Info, + ParamsWithLegacyArgs). %%%---------------------------------------------------------------------- From 86bd66917dc70f7642028b1e05c4135a6f171a08 Mon Sep 17 00:00:00 2001 From: jacekwegr Date: Mon, 3 Oct 2022 16:22:37 +0200 Subject: [PATCH 098/117] More specific CLI help --- priv/graphql/schemas/admin/account.gql | 6 +-- priv/graphql/schemas/admin/admin_schema.gql | 42 ++++++++++---------- priv/graphql/schemas/admin/last.gql | 4 +- priv/graphql/schemas/admin/metric.gql | 10 ++--- priv/graphql/schemas/admin/mnesia.gql | 7 +++- priv/graphql/schemas/admin/muc_light.gql | 4 +- priv/graphql/schemas/admin/offline.gql | 4 +- priv/graphql/schemas/admin/private.gql | 8 ++-- priv/graphql/schemas/admin/roster.gql | 4 +- priv/graphql/schemas/admin/server.gql | 4 +- priv/graphql/schemas/admin/stanza.gql | 9 +++-- priv/graphql/schemas/admin/stats.gql | 20 +++++----- priv/graphql/schemas/admin/token.gql | 4 +- priv/graphql/schemas/admin/vcard.gql | 4 +- priv/graphql/schemas/global/muc.gql | 16 ++++---- priv/graphql/schemas/global/scalar_types.gql | 2 +- priv/graphql/schemas/global/stanza.gql | 2 +- src/ejabberd_ctl.erl | 4 +- 18 files changed, 80 insertions(+), 74 deletions(-) diff --git a/priv/graphql/schemas/admin/account.gql b/priv/graphql/schemas/admin/account.gql index 6856001d007..bb2bdeeec76 100644 --- a/priv/graphql/schemas/admin/account.gql +++ b/priv/graphql/schemas/admin/account.gql @@ -26,14 +26,14 @@ type AccountAdminMutation @protected{ "Register a user. Username will be generated when skipped" registerUser(domain: String!, username: String, password: String!): UserPayload @protected(type: DOMAIN, args: ["domain"]) - "Remove a user" + "Remove the user's account along with all the associated personal data" removeUser(user: JID!): UserPayload @protected(type: DOMAIN, args: ["user"]) "Ban an account: kick sessions and set a random password" - banUser(user: JID!, reason: String!): UserPayload + banUser(user: JID!, reason: String!): UserPayload @protected(type: DOMAIN, args: ["user"]) "Change the password of a user" - changeUserPassword(user: JID!, newPassword: String!): UserPayload + changeUserPassword(user: JID!, newPassword: String!): UserPayload @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index fd7bf8b9dc2..6209c281d07 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -12,7 +12,7 @@ type AdminQuery{ checkAuth: AdminAuthInfo "Account management" account: AccountAdminQuery - "Domain management" + "Dynamic domain management" domain: DomainAdminQuery "Last activity management" last: LastAdminQuery @@ -20,25 +20,25 @@ type AdminQuery{ muc: MUCAdminQuery "MUC Light room management" muc_light: MUCLightAdminQuery - "Session management" + "User session management" session: SessionAdminQuery - "Stanza management" + "Sending stanzas and querying MAM" stanza: StanzaAdminQuery - "Roster/Contacts management" + "User roster/contacts management" roster: RosterAdminQuery - "Vcard management" + "vCard management" vcard: VcardAdminQuery - "Private storage management" + "User private storage management" private: PrivateAdminQuery - "Metrics management" + "Browse metrics" metric: MetricAdminQuery - "Statistics" + "Server statistics" stat: StatsAdminQuery "Personal data management according to GDPR" gdpr: GdprAdminQuery "Mnesia internal database management" mnesia: MnesiaAdminQuery - "Server management" + "Server info and management" server: ServerAdminQuery } @@ -49,34 +49,34 @@ Only an authenticated admin can execute these mutations. type AdminMutation @protected{ "Account management" account: AccountAdminMutation - "Domain management" + "Dynamic domain management" domain: DomainAdminMutation - "Inbox bin management" + "Inbox bin flushing" inbox: InboxAdminMutation - "Last activity management" + "Last user activity management" last: LastAdminMutation "MUC room management" muc: MUCAdminMutation "MUC Light room management" muc_light: MUCLightAdminMutation - "Session management" + "User session management" session: SessionAdminMutation - "Stanza management" + "Sending stanzas and querying MAM" stanza: StanzaAdminMutation - "Roster/Contacts management" + "User roster/contacts management" roster: RosterAdminMutation - "Vcard management" + "vCard management" vcard: VcardAdminMutation - "Private storage management" + "User private storage management" private: PrivateAdminMutation - "Http upload" + "Generating upload/download URLs for the files" httpUpload: HttpUploadAdminMutation - "Offline deleting old messages" + "Deleting old Offline messages" offline: OfflineAdminMutation - "OAUTH token management" + "OAUTH user token management" token: TokenAdminMutation "Mnesia internal database management" mnesia: MnesiaAdminMutation - "Server management" + "Server info and management" server: ServerAdminMutation } diff --git a/priv/graphql/schemas/admin/last.gql b/priv/graphql/schemas/admin/last.gql index 30e6c0a3761..ac003c17093 100644 --- a/priv/graphql/schemas/admin/last.gql +++ b/priv/graphql/schemas/admin/last.gql @@ -2,7 +2,7 @@ Allow admin to manage last activity. """ type LastAdminQuery @use(modules: ["mod_last"]) @protected{ - "Get the user's last activity information" + "Get the user's last status and timestamp" getLast(user: JID!): LastActivity @use(arg: "user") @protected(type: DOMAIN, args: ["user"]) "Get the number of users active from the given timestamp" @@ -20,7 +20,7 @@ type LastAdminQuery @use(modules: ["mod_last"]) @protected{ Allow admin to get information about last activity. """ type LastAdminMutation @use(modules: ["mod_last"]) @protected{ - "Set user's last activity information" + "Set user's last status and timestamp" setLast(user: JID!, timestamp: DateTime, status: String!): LastActivity @use(arg: "user") @protected(type: DOMAIN, args: ["user"]) """ diff --git a/priv/graphql/schemas/admin/metric.gql b/priv/graphql/schemas/admin/metric.gql index 6c49e7fad6f..db2772d1b7a 100644 --- a/priv/graphql/schemas/admin/metric.gql +++ b/priv/graphql/schemas/admin/metric.gql @@ -2,7 +2,7 @@ Result of a metric """ enum MetricType { - "Collects values over a sliding window of 60s and returns apropriate statistical values" + "Collects values over a sliding window of 60s and returns appropriate statistical values" histogram "Returns a number" counter @@ -13,13 +13,13 @@ enum MetricType { spiral "Consists of value and time in milliseconds elapsed from the last metric update" gauge - "Informations about the inet" + "TCP/IP connection statistics from the 'inet' module" merged_inet_stats - "Metrics of the relational database management sytem" + "Metrics of the relational database management system" rdbms_stats - "Metrics of the virual machine memory" + "Metrics of the virtual machine memory" vm_stats_memory - "Information about virual machine" + "Information about virtual machine" vm_system_info "Information about process queue length" probe_queues diff --git a/priv/graphql/schemas/admin/mnesia.gql b/priv/graphql/schemas/admin/mnesia.gql index 85afbac760a..1a859a23534 100644 --- a/priv/graphql/schemas/admin/mnesia.gql +++ b/priv/graphql/schemas/admin/mnesia.gql @@ -2,7 +2,10 @@ Allow admin to acquire information about mnesia database """ type MnesiaAdminQuery @protected{ - "Allow to acquire information about mnesia database" + """ + Get the information about appropriate mnesia property for a specified key, + if no keys are provided all the available properties will be returned + """ systemInfo(keys: [String!]): [MnesiaInfo] @protected(type: GLOBAL) } @@ -56,7 +59,7 @@ type MnesiaListResponse { key: String } -"Mnesia response in the form of a integer" +"Mnesia response in the form of an integer" type MnesiaIntResponse { "Result as an integer" result: Int diff --git a/priv/graphql/schemas/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql index 37b5cd8fa24..26f0bbda88b 100644 --- a/priv/graphql/schemas/admin/muc_light.gql +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -20,7 +20,7 @@ type MUCLightAdminMutation @protected{ "Send a message to a MUC Light room" sendMessageToRoom(room: JID!, from: JID!, body: String!): String @protected(type: DOMAIN, args: ["from"]) - "Set the user blocking list" + "Set the user's list of blocked entities" setBlockingList(user: JID!, items: [BlockingInput!]!): String @protected(type: DOMAIN, args: ["user"]) } @@ -41,7 +41,7 @@ type MUCLightAdminQuery @protected{ "Get the list of MUC Light rooms that the user participates in" listUserRooms(user: JID!): [JID!] @protected(type: DOMAIN, args: ["user"]) - "Get the user blocking list" + "Get the user's list of blocked entities" getBlockingList(user: JID!): [BlockingItem!] @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/admin/offline.gql b/priv/graphql/schemas/admin/offline.gql index 9e1b15a1845..fc4c7238627 100644 --- a/priv/graphql/schemas/admin/offline.gql +++ b/priv/graphql/schemas/admin/offline.gql @@ -2,10 +2,10 @@ Allow admin to delete offline messages from specified domain """ type OfflineAdminMutation @protected{ - "Allow admin to delete offline messages whose date has expired" + "Delete offline messages whose date has expired" deleteExpiredMessages(domain: String!): String @protected(type: DOMAIN, args: ["domain"]) - "Allow the admin to delete messages at least as old as the number of days specified in the parameter" + "Delete messages at least as old as the number of days specified in the parameter" deleteOldMessages(domain: String!, days: Int!): String @protected(type: DOMAIN, args: ["domain"]) } diff --git a/priv/graphql/schemas/admin/private.gql b/priv/graphql/schemas/admin/private.gql index ec5ce5857d9..01d3d83f3ac 100644 --- a/priv/graphql/schemas/admin/private.gql +++ b/priv/graphql/schemas/admin/private.gql @@ -1,17 +1,17 @@ """ -Allow admin to set user's private +Allow admin to set the user's private data """ type PrivateAdminMutation @protected { - "Set user's private" + "Set the user's private data" setPrivate(user: JID!, elementString: String!): String @protected(type: DOMAIN, args: ["user"]) } """ -Allow admin to get user's private +Allow admin to get the user's private data """ type PrivateAdminQuery @protected { - "Get user's private" + "Get the user's private data" getPrivate(user: JID!, element: String!, nameSpace: String!): String @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/admin/roster.gql b/priv/graphql/schemas/admin/roster.gql index e9927746845..5b68d2c7bb7 100644 --- a/priv/graphql/schemas/admin/roster.gql +++ b/priv/graphql/schemas/admin/roster.gql @@ -1,5 +1,5 @@ """ -Allow admin to manage user rester/contacts. +Allow admin to manage user roster/contacts. """ type RosterAdminMutation @protected{ "Add a new contact to a user's roster without subscription" @@ -35,7 +35,7 @@ type RosterAdminQuery @protected{ "Get the user's roster/contacts" listContacts(user: JID!): [Contact!] @protected(type: DOMAIN, args: ["user"]) - "Get the user's contact" + "Get the information about the user's specific contact" getContact(user: JID!, contact: JID!): Contact @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/admin/server.gql b/priv/graphql/schemas/admin/server.gql index 353ca248c21..245a0f32a5f 100644 --- a/priv/graphql/schemas/admin/server.gql +++ b/priv/graphql/schemas/admin/server.gql @@ -5,7 +5,7 @@ type ServerAdminQuery @protected{ "Get the status of the server" status: Status @protected(type: GLOBAL) - "Get the loglevel of the server" + "Get MongooseIM node's current LogLevel" getLoglevel: LogLevel @protected(type: GLOBAL) "Get the Erlang cookie of this node" @@ -35,7 +35,7 @@ type ServerAdminMutation @protected{ "Remove a MongooseIM node from Mnesia clustering config" removeNode(node: String!): String @protected(type: GLOBAL) - "Set MongooseIM Node's loglevel" + "Set MongooseIM node's LogLevel" setLoglevel(level: LogLevel!): String @protected(type: GLOBAL) } diff --git a/priv/graphql/schemas/admin/stanza.gql b/priv/graphql/schemas/admin/stanza.gql index 9a562ed3752..151c1629b89 100644 --- a/priv/graphql/schemas/admin/stanza.gql +++ b/priv/graphql/schemas/admin/stanza.gql @@ -1,14 +1,17 @@ type StanzaAdminQuery @protected{ - "Get n last messages to/from a given contact (optional) with limit and optional date" + """ + Get last 50 messages to/from a given contact, optionally you can change the limit, + specify a date or select only messages exchanged with a specific contact + """ getLastMessages(caller: JID!, with: JID, limit: Int = 50, before: DateTime): StanzasPayload @protected(type: DOMAIN, args: ["caller"]) } type StanzaAdminMutation @protected{ - "Send a chat message to a local or remote bare or full JID" + "Send a chat message from a given contact to a local or remote bare or full JID" sendMessage(from: JID!, to: JID!, body: String!): SendStanzaPayload @protected(type: DOMAIN, args: ["from"]) - "Send a headline message to a local or remote bare or full JID" + "Send a headline message from a given contact to a local or remote bare or full JID" sendMessageHeadLine(from: JID!, to: JID!, subject: String, body: String): SendStanzaPayload @protected(type: DOMAIN, args: ["from"]) "Send an arbitrary stanza. Only for global admin" diff --git a/priv/graphql/schemas/admin/stats.gql b/priv/graphql/schemas/admin/stats.gql index c0658f09d5a..a0dfecd78b7 100644 --- a/priv/graphql/schemas/admin/stats.gql +++ b/priv/graphql/schemas/admin/stats.gql @@ -1,31 +1,31 @@ "Allow admin to get statistics" type StatsAdminQuery @protected{ - "allow admin to acquire all nodes' statistics" + "Get statistics from all of the nodes. Only for global admin" globalStats: GlobalStats @protected(type: GLOBAL) - "allow admin to acquire domain's statistics" + "Get statistics from a specific domain" domainStats(domain: String!): DomainStats @protected(type: DOMAIN, args: ["domain"]) } type GlobalStats { - "uptime of the node" + "Uptime of the node" uptimeSeconds: Int - "number of registered users" + "Number of registered users" registeredUsers: Int - "number of online users on the node" + "Number of online users on the node" onlineUsersNode: Int - "number of online users" + "Number of online users" onlineUsers: Int - "number of all incoming s2s connections" + "Number of all incoming s2s connections" incomingS2S: Int - "number of all outgoing s2s connections" + "Number of all outgoing s2s connections" outgoingS2S: Int } type DomainStats { - "number of registered users on a given domain" + "Number of registered users on a given domain" registeredUsers: Int - "number of online users on a given domain" + "Number of online users on a given domain" onlineUsers: Int } diff --git a/priv/graphql/schemas/admin/token.gql b/priv/graphql/schemas/admin/token.gql index 366d4623c73..0f94b9a0cb5 100644 --- a/priv/graphql/schemas/admin/token.gql +++ b/priv/graphql/schemas/admin/token.gql @@ -2,10 +2,10 @@ Allow admin to get and revoke user's auth tokens """ type TokenAdminMutation @protected { - "Request auth token for an user" + "Request auth token for a user" requestToken(user: JID!): Token @protected(type: DOMAIN, args: ["user"]) - "Revoke any tokens for an user" + "Revoke any tokens for a user" revokeToken(user: JID!): String @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/admin/vcard.gql b/priv/graphql/schemas/admin/vcard.gql index 1eb788a9257..b7eb69319fe 100644 --- a/priv/graphql/schemas/admin/vcard.gql +++ b/priv/graphql/schemas/admin/vcard.gql @@ -8,10 +8,10 @@ type VcardAdminMutation @protected{ } """ -Allow admin to get user's vcard +Allow admin to get the user's vcard """ type VcardAdminQuery @protected{ - "Get user's vcard" + "Get the user's vcard" getVcard(user: JID!): Vcard @protected(type: DOMAIN, args: ["user"]) } diff --git a/priv/graphql/schemas/global/muc.gql b/priv/graphql/schemas/global/muc.gql index d14a32ac348..6448258c6d5 100644 --- a/priv/graphql/schemas/global/muc.gql +++ b/priv/graphql/schemas/global/muc.gql @@ -1,10 +1,10 @@ -"User affilation to a specific room" +"User affiliation to a specific room" enum MUCAffiliation{ "The user is the owner of the room" OWNER - "The user has administrative role" + "The user has an administrative role" ADMIN - "The user is a member of the rooom" + "The user is a member of the room" MEMBER "The user isn't a member of the room" OUTCAST @@ -14,7 +14,7 @@ enum MUCAffiliation{ "MUC role types" enum MUCRole{ - "User is a visiting" + "User is a visitor" VISITOR "User can participate in the room" PARTICIPANT @@ -46,7 +46,7 @@ type MUCRoomDesc{ jid: JID! "Room's title" title: String! - "Is room private?" + "Is the room private?" private: Boolean "Number of the users" usersNumber: Int @@ -58,13 +58,13 @@ type MUCRoomConfig{ title: String!, "Room's description" description: String!, - "Allow to change room's subject?" + "Allow to change the room's subject?" allowChangeSubject: Boolean!, "Allow to query users?" allowQueryUsers: Boolean!, "Allow private messages?" allowPrivateMessages: Boolean!, - "Allow visitor staus?" + "Allow visitor status?" allowVisitorStatus: Boolean!, "Allow visitors to change their nicks?" allowVisitorNickchange: Boolean!, @@ -110,7 +110,7 @@ input MUCRoomConfigInput{ allowQueryUsers: Boolean, "Allow private messages?" allowPrivateMessages: Boolean, - "Allow visitor staus?" + "Allow visitor status?" allowVisitorStatus: Boolean, "Allow visitors to change their nicks?" allowVisitorNickchange: Boolean, diff --git a/priv/graphql/schemas/global/scalar_types.gql b/priv/graphql/schemas/global/scalar_types.gql index 6b8d81bad48..0c06fdfc9d1 100644 --- a/priv/graphql/schemas/global/scalar_types.gql +++ b/priv/graphql/schemas/global/scalar_types.gql @@ -2,7 +2,7 @@ scalar DateTime "Body of a data structure exchanged by XMPP entities in XML streams" scalar Stanza @spectaql(options: [{ key: "example", value: "Hi!" }]) -"Unique indetifier in the form of **node@domain**" +"Unique identifier in the form of **node@domain**" scalar JID @spectaql(options: [{ key: "example", value: "alice@localhost" }]) "The JID with resource" scalar FullJID @spectaql(options: [{ key: "example", value: "alice@localhost/res1" }]) diff --git a/priv/graphql/schemas/global/stanza.gql b/priv/graphql/schemas/global/stanza.gql index d015d1aa463..22eb60c9c08 100644 --- a/priv/graphql/schemas/global/stanza.gql +++ b/priv/graphql/schemas/global/stanza.gql @@ -20,6 +20,6 @@ type StanzaMap{ "Send stanza payload" type SendStanzaPayload{ - "Send stanza id" + "Stanza id" id: ID } diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 67427231f8c..da9b62c9bf1 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -567,8 +567,8 @@ basic_commands() -> {"stop", [], "Stop MongooseIM"}, {"restart", [], "Restart MongooseIM"}, {"help", ["[--tags [tag] | com?*]"], "Show help for the deprecated commands"}, - {"mnesia", ["[info]"], "show information of Mnesia system"}, - {"graphql", ["query"], "Execute graphql query or mutation"}]. + {"mnesia", ["[info]"], "Show information about Mnesia database management system"}, + {"graphql", ["query"], "Execute GraphQL query or mutation"}]. -spec print_categories(dual | long, MaxC :: integer(), ShCode :: boolean()) -> ok. print_categories(HelpMode, MaxC, ShCode) -> From 194afcceeef9d309008f91ea93e0cf9ba069a9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20D=C5=82ugosz?= Date: Wed, 5 Oct 2022 08:29:43 +0200 Subject: [PATCH 099/117] Refactored hook handler in mod_global_distrib_disco module --- .../mod_global_distrib_disco.erl | 24 ++++++++++--------- src/mongoose_hooks.erl | 3 ++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/global_distrib/mod_global_distrib_disco.erl b/src/global_distrib/mod_global_distrib_disco.erl index 31e530b203c..389feaf3e7a 100644 --- a/src/global_distrib/mod_global_distrib_disco.erl +++ b/src/global_distrib/mod_global_distrib_disco.erl @@ -23,9 +23,7 @@ -include("mongoose.hrl"). -include("jlib.hrl"). --export([start/2, stop/1, deps/2, disco_local_items/1]). - --ignore_xref([disco_local_items/1]). +-export([start/2, stop/1, deps/2, disco_local_items/3]). %%-------------------------------------------------------------------- %% API @@ -33,11 +31,11 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). start(HostType, _Opts) -> - ejabberd_hooks:add(hooks(HostType)). + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> any(). stop(HostType) -> - ejabberd_hooks:delete(hooks(HostType)). + gen_hook:delete_handlers(hooks(HostType)). -spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps(). deps(_HostType, Opts) -> @@ -47,21 +45,25 @@ deps(_HostType, Opts) -> %% Hooks implementation %%-------------------------------------------------------------------- --spec disco_local_items(mongoose_disco:item_acc()) -> mongoose_disco:item_acc(). -disco_local_items(Acc = #{host_type := HostType, from_jid := From, node := <<>>}) -> +-spec disco_local_items(Acc, Params, Extra) -> {ok, Acc} when + Acc :: mongoose_disco:item_acc(), + Params :: map(), + Extra :: map(). +disco_local_items(Acc = #{host_type := HostType, from_jid := From, node := <<>>}, _, _) -> Domains = domains_for_disco(HostType, From), ?LOG_DEBUG(#{what => gd_domains_fetched_for_disco, domains => Domains}), Items = [#{jid => Domain} || Domain <- Domains], - mongoose_disco:add_items(Items, Acc); -disco_local_items(Acc) -> - Acc. + NewAcc = mongoose_disco:add_items(Items, Acc), + {ok, NewAcc}; +disco_local_items(Acc, _, _) -> + {ok, Acc}. %%-------------------------------------------------------------------- %% Helpers %%-------------------------------------------------------------------- hooks(HostType) -> - [{disco_local_items, HostType, ?MODULE, disco_local_items, 99}]. + [{disco_local_items, HostType, fun ?MODULE:disco_local_items/3, #{}, 99}]. -spec domains_for_disco(mongooseim:host_type(), From :: jid:jid()) -> Domains :: [binary()]. domains_for_disco(_HostType, #jid{ luser = <<>> } = _From) -> diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index abd4b2d4145..c3e3a2e3849 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -1309,7 +1309,8 @@ disco_sm_identity(Acc = #{host_type := HostType}) -> %%% @doc `disco_local_items' hook is called to extract items associated with the server. -spec disco_local_items(mongoose_disco:item_acc()) -> mongoose_disco:item_acc(). disco_local_items(Acc = #{host_type := HostType}) -> - run_hook_for_host_type(disco_local_items, HostType, Acc, []). + ParamsWithLegacyArgs = ejabberd_hooks:add_args(#{}, []), + run_hook_for_host_type(disco_local_items, HostType, Acc, ParamsWithLegacyArgs). %%% @doc `disco_sm_items' hook is called to get the items associated %%% with the client when a discovery IQ gets to session management. From 571e33350c0edfb90c5dfbb28ee37effc459b3a7 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Fri, 19 Aug 2022 14:38:10 +0200 Subject: [PATCH 100/117] Adding use annotation to mod_http_upload and deleting if mod_http_upload is loaded on Host --- big_tests/tests/graphql_http_upload_SUITE.erl | 8 ++++---- priv/graphql/schemas/admin/http_upload.gql | 4 ++-- priv/graphql/schemas/user/http_upload.gql | 4 ++-- src/http_upload/mod_http_upload_api.erl | 19 +++++++------------ 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/big_tests/tests/graphql_http_upload_SUITE.erl b/big_tests/tests/graphql_http_upload_SUITE.erl index dd63b0bcf3b..ca35bee7ea2 100644 --- a/big_tests/tests/graphql_http_upload_SUITE.erl +++ b/big_tests/tests/graphql_http_upload_SUITE.erl @@ -184,8 +184,8 @@ user_http_upload_not_configured(Config) -> user_http_upload_not_configured(Config, Alice) -> Result = user_get_url(<<"test">>, 123, <<"Test">>, 123, Alice, Config), - ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)), - ?assertEqual(<<"mod_http_upload is not loaded for this host">>, get_err_msg(Result)). + ?assertEqual(<<"deps_not_loaded">>, get_err_code(Result)), + ?assertEqual(<<"Some of required modules or services are not loaded">>, get_err_msg(Result)). % Admin test cases @@ -218,8 +218,8 @@ admin_get_url_no_domain(Config) -> admin_http_upload_not_configured(Config) -> Result = admin_get_url(domain(), <<"test">>, 123, <<"Test">>, 123, Config), - ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)), - ?assertEqual(<<"mod_http_upload is not loaded for this host">>, get_err_msg(Result)). + ?assertEqual(<<"deps_not_loaded">>, get_err_code(Result)), + ?assertEqual(<<"Some of required modules or services are not loaded">>, get_err_msg(Result)). domain_admin_get_url_no_permission(Config) -> Result1 = admin_get_url(<<"AAAAA">>, <<"test">>, 123, <<"Test">>, 123, Config), diff --git a/priv/graphql/schemas/admin/http_upload.gql b/priv/graphql/schemas/admin/http_upload.gql index 6690cc9f7f9..fccffa1dca5 100644 --- a/priv/graphql/schemas/admin/http_upload.gql +++ b/priv/graphql/schemas/admin/http_upload.gql @@ -1,8 +1,8 @@ """ Allow admin to generate upload/download URL for a file on user's behalf". """ -type HttpUploadAdminMutation @protected{ +type HttpUploadAdminMutation @use(modules: ["mod_http_upload"]) @protected{ "Allow admin to generate upload/download URLs for a file on user's behalf" getUrl(domain: String!, filename: String!, size: Int!, contentType: String!, timeout: Int!): FileUrls - @protected(type: DOMAIN, args: ["domain"]) + @use(arg: "domain") @protected(type: DOMAIN, args: ["domain"]) } diff --git a/priv/graphql/schemas/user/http_upload.gql b/priv/graphql/schemas/user/http_upload.gql index 665a30c2d25..3a4c141eeac 100644 --- a/priv/graphql/schemas/user/http_upload.gql +++ b/priv/graphql/schemas/user/http_upload.gql @@ -1,7 +1,7 @@ """ Allow user to generate upload/download URL for a file". """ -type HttpUploadUserMutation @protected{ +type HttpUploadUserMutation @use(modules: ["mod_http_upload"]) @protected{ "Allow user to generate upload/download URLs for a file" - getUrl(filename: String!, size: Int!, contentType: String!, timeout: Int!): FileUrls + getUrl(filename: String!, size: Int!, contentType: String!, timeout: Int!): FileUrls @use } diff --git a/src/http_upload/mod_http_upload_api.erl b/src/http_upload/mod_http_upload_api.erl index 35acc2e0c12..702f9f06339 100644 --- a/src/http_upload/mod_http_upload_api.erl +++ b/src/http_upload/mod_http_upload_api.erl @@ -34,18 +34,13 @@ get_urls(Domain, Filename, Size, ContentType, Timeout) -> end. check_module_and_get_urls(HostType, Filename, Size, ContentType, Timeout) -> - case gen_mod:is_loaded(HostType, mod_http_upload) of - true -> - case mod_http_upload:get_urls(HostType, Filename, Size, ContentType, Timeout) of - {PutURL, GetURL, Header} -> - {ok, #{<<"PutUrl">> => PutURL, <<"GetUrl">> => GetURL, - <<"Header">> => header_output(Header)}}; - file_too_large_error -> - {file_too_large_error, - "Declared file size exceeds the host's maximum file size."} - end; - false -> - {module_not_loaded_error, "mod_http_upload is not loaded for this host"} + case mod_http_upload:get_urls(HostType, Filename, Size, ContentType, Timeout) of + {PutURL, GetURL, Header} -> + {ok, #{<<"PutUrl">> => PutURL, <<"GetUrl">> => GetURL, + <<"Header">> => header_output(Header)}}; + file_too_large_error -> + {file_too_large_error, + "Declared file size exceeds the host's maximum file size."} end. -spec generate_output_message(PutURL :: binary(), GetURL :: binary(), From cb6978972fcf2b7062e16ffa7a01778cca82db5f Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 24 Aug 2022 14:37:13 +0200 Subject: [PATCH 101/117] Adding tests to graphql_inbox --- big_tests/tests/graphql_helper.erl | 4 ++ big_tests/tests/graphql_inbox_SUITE.erl | 91 +++++++++++++++++++++---- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index b8216b93f89..09d00ea3d98 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -156,6 +156,10 @@ get_listener_opts(EpName) -> #{handlers := [Opts]} = get_listener_config(Node, EpName), Opts. +get_not_loaded(Resp) -> + ?assertEqual(<<"deps_not_loaded">>, get_err_code(Resp)), + ?assertEqual(<<"Some of required modules or services are not loaded">>, get_err_msg(Resp)). + get_err_code(Resp) -> get_value([extensions, code], get_error(1, Resp)). diff --git a/big_tests/tests/graphql_inbox_SUITE.erl b/big_tests/tests/graphql_inbox_SUITE.erl index 3ce31cdaeca..135bc239ca2 100644 --- a/big_tests/tests/graphql_inbox_SUITE.erl +++ b/big_tests/tests/graphql_inbox_SUITE.erl @@ -3,8 +3,10 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). +-import(domain_helper, [host_type/0, domain/0]). -import(graphql_helper, [execute_user_command/5, execute_command/4, user_to_bin/1, - get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1]). + get_ok_value/2, get_err_msg/1, get_err_code/1, get_not_loaded/1, + get_unauthorized/1]). -include_lib("eunit/include/eunit.hrl"). -include("inbox.hrl"). @@ -19,20 +21,35 @@ all() -> inbox_helper:skip_or_run_inbox_tests(tests()). tests() -> - [{group, user_inbox}, - {group, admin_inbox_http}, - {group, admin_inbox_cli}, + [{group, user}, + {group, admin_http}, + {group, admin_cli}, {group, domain_admin_inbox}]. groups() -> - [{user_inbox, [], user_inbox_tests()}, - {admin_inbox_http, [], admin_inbox_tests()}, - {admin_inbox_cli, [], admin_inbox_tests()}, + [{user, [], user_groups()}, + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {user_inbox, [], user_inbox_tests()}, + {user_inbox_not_configured, [], user_inbox_not_configured_tests()}, + {admin_inbox, [], admin_inbox_tests()}, + {admin_inbox_not_configured, [], admin_inbox_not_configured_tests()}, {domain_admin_inbox, [], domain_admin_inbox_tests()}]. +user_groups() -> + [{group, user_inbox}, + {group, user_inbox_not_configured}]. + +admin_groups() -> + [{group, admin_inbox}, + {group, domain_admin_inbox}]. + user_inbox_tests() -> [user_flush_own_bin]. +user_inbox_not_configured_tests() -> + [user_flush_own_bin_inbox_not_configured]. + admin_inbox_tests() -> [admin_flush_user_bin, admin_try_flush_nonexistent_user_bin, @@ -51,13 +68,15 @@ domain_admin_inbox_tests() -> domain_admin_try_flush_domain_bin_no_permission, domain_admin_flush_global_bin_no_permission]. +admin_inbox_not_configured_tests() -> + [admin_flush_user_bin_inbox_not_configured, + admin_flush_domain_bin_inbox_not_configured, + admin_flush_global_bin_inbox_not_configured]. + init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), Config1 = dynamic_modules:save_modules([HostType, SecHostType], Config), - Modules = [{mod_inbox, inbox_helper:inbox_opts(async_pools)} | inbox_helper:muclight_modules()], - ok = dynamic_modules:ensure_modules(HostType, Modules), - ok = dynamic_modules:ensure_modules(SecHostType, Modules), Config2 = ejabberd_node_utils:init(mim(), Config1), escalus:init_per_suite(Config2). @@ -65,14 +84,34 @@ end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_per_group(admin_inbox_http, Config) -> +init_per_group(user, Config) -> + graphql_helper:init_user(Config); +init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); -init_per_group(admin_inbox_cli, Config) -> +init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin_inbox, Config) -> + ensure_inbox_started(), graphql_helper:init_domain_admin_handler(Config); -init_per_group(user_inbox, Config) -> - graphql_helper:init_user(Config). +init_per_group(Group, Config) when Group =:= user_inbox; + Group =:= admin_inbox -> + ensure_inbox_started(), + Config; +init_per_group(Group, Config) when Group =:= user_inbox_not_configured; + Group =:= admin_inbox_not_configured -> + HostType = domain_helper:host_type(), + SecHostType = domain_helper:secondary_host_type(), + Modules = [{mod_inbox, stopped}], + ok = dynamic_modules:ensure_modules(HostType, Modules), + ok = dynamic_modules:ensure_modules(SecHostType, Modules), + Config. + +ensure_inbox_started() -> + HostType = domain_helper:host_type(), + SecHostType = domain_helper:secondary_host_type(), + Modules = [{mod_inbox, inbox_helper:inbox_opts(async_pools)} | inbox_helper:muclight_modules()], + ok = dynamic_modules:ensure_modules(HostType, Modules), + ok = dynamic_modules:ensure_modules(SecHostType, Modules). end_per_group(_, _) -> graphql_helper:clean(). @@ -159,6 +198,21 @@ admin_try_flush_nonexistent_host_type_bin(Config) -> ?assertErrMsg(Res, <<"not found">>), ?assertErrCode(Res, host_type_not_found). +% Admin inbox not configured test cases + +admin_flush_user_bin_inbox_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_flush_user_bin_inbox_not_configured/2). + +admin_flush_user_bin_inbox_not_configured(Config, Alice) -> + get_not_loaded(flush_user_bin(Alice, Config)). + +admin_flush_domain_bin_inbox_not_configured(Config) -> + get_not_loaded(flush_domain_bin(domain(), Config)). + +admin_flush_global_bin_inbox_not_configured(Config) -> + get_not_loaded(flush_global_bin(host_type(), 10, Config)). + %% Domain admin test cases domain_admin_flush_user_bin_no_permission(Config) -> @@ -199,6 +253,15 @@ user_flush_own_bin(Config, Alice, Bob, Kate) -> ?assertEqual(1, NumOfRows), inbox_helper:check_inbox(Bob, [], #{box => bin}). +% User inbox not configured test cases + +user_flush_own_bin_inbox_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_flush_own_bin_inbox_not_configured/2). + +user_flush_own_bin_inbox_not_configured(Config, Alice) -> + get_not_loaded(user_flush_own_bin(Alice, Config)). + %% Helpers create_room_and_make_users_leave(Alice, Bob, Kate) -> From fb5454acf8a84fe8349fbd998ffa85ac7ea966ac Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Mon, 12 Sep 2022 10:27:04 +0200 Subject: [PATCH 102/117] Adding @use annotation tests in graphql_last_SUITE --- big_tests/tests/graphql_last_SUITE.erl | 122 +++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 457e378b60d..81e31a0e3ec 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -4,7 +4,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, user_to_jid/1, - get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1]). + get_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1, + get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). @@ -25,16 +26,28 @@ all() -> {group, domain_admin}]. groups() -> - [{user, [parallel], user_tests()}, + [{user, [], user_groups()}, + {user_last, [parallel], user_tests()}, {admin_http, [], admin_groups()}, {admin_cli, [], admin_groups()}, {domain_admin, [], domain_admin_groups()}, {admin_last, [], admin_last_tests()}, {domain_admin_last, [], domain_admin_last_tests()}, {admin_old_users, [], admin_old_users_tests()}, - {domain_admin_old_users, [], domain_admin_old_users_tests()}]. + {domain_admin_old_users, [], domain_admin_old_users_tests()}, + {admin_last_not_configured, [], admin_last_not_configured()}, + {admin_last_configured, [], admin_last_configured()}, + {user_last_not_configured, [], user_last_not_configured()}]. + +user_groups() -> + [{group, user_last}, + {group, user_last_not_configured}]. admin_groups() -> + [{group, admin_last_configured}, + {group, admin_last_not_configured}]. + +admin_last_configured() -> [{group, admin_last}, {group, admin_old_users}]. @@ -47,6 +60,10 @@ user_tests() -> user_get_last, user_get_other_user_last]. +user_last_not_configured() -> + [user_set_last_not_configured, + user_get_last_not_configured]. + admin_last_tests() -> [admin_set_last, admin_try_set_nonexistent_user_last, @@ -85,16 +102,21 @@ domain_admin_old_users_tests() -> domain_admin_user_without_last_info_is_old_user, domain_admin_logged_user_is_not_old_user]. +admin_last_not_configured() -> + [admin_set_last_not_configured, + admin_get_last_not_configured, + admin_count_active_users_last_not_configured, + admin_remove_old_users_domain_last_not_configured, + admin_remove_old_users_global_last_not_configured, + admin_list_old_users_domain_last_not_configured, + admin_list_old_users_global_last_not_configured]. + init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), Config1 = escalus:init_per_suite(Config), Config2 = dynamic_modules:save_modules([HostType, SecHostType], Config1), Config3 = ejabberd_node_utils:init(mim(), Config2), - Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), - SecBackend = mongoose_helper:get_backend_mnesia_rdbms_riak(SecHostType), - dynamic_modules:ensure_modules(HostType, required_modules(Backend)), - dynamic_modules:ensure_modules(SecHostType, required_modules(SecBackend)), escalus:init_per_suite(Config3). end_per_suite(Config) -> @@ -108,11 +130,20 @@ init_per_group(admin_http, Config) -> init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin, Config) -> + configure_last(Config), graphql_helper:init_domain_admin_handler(Config); -init_per_group(admin_last, Config) -> - Config; init_per_group(domain_admin_last, Config) -> Config; +init_per_group(user_last, Config) -> + configure_last(Config); +init_per_group(user_last_not_configured, Config) -> + stop_last(Config); +init_per_group(admin_last, Config) -> + Config; +init_per_group(admin_last_configured, Config) -> + configure_last(Config); +init_per_group(admin_last_not_configured, Config) -> + stop_last(Config); init_per_group(admin_old_users, Config) -> AuthMods = mongoose_helper:auth_modules(), case lists:member(ejabberd_auth_ldap, AuthMods) of @@ -126,6 +157,22 @@ init_per_group(domain_admin_old_users, Config) -> false -> Config end. +configure_last(Config) -> + HostType = domain_helper:host_type(), + SecHostType = domain_helper:secondary_host_type(), + Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), + SecBackend = mongoose_helper:get_backend_mnesia_rdbms_riak(SecHostType), + dynamic_modules:ensure_modules(HostType, required_modules(Backend)), + dynamic_modules:ensure_modules(SecHostType, required_modules(SecBackend)), + Config. + +stop_last(Config) -> + HostType = domain_helper:host_type(), + SecHostType = domain_helper:secondary_host_type(), + dynamic_modules:ensure_modules(HostType, [{mod_last, stopped}]), + dynamic_modules:ensure_modules(SecHostType, [{mod_last, stopped}]), + Config. + end_per_group(GroupName, _Config) when GroupName =:= admin_http; GroupName =:= admin_cli -> graphql_helper:clean(); @@ -445,6 +492,63 @@ user_get_other_user_last_story(Config, Alice, Bob) -> #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = get_ok_value(p(getLast), Res). + +admin_set_last_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_set_last_not_configured_story/2). + +admin_set_last_not_configured_story(Config, Alice) -> + Status = <<"First status">>, + Res = admin_set_last(Alice, Status, ?DEFAULT_DT, Config), + get_not_loaded(Res). + +admin_get_last_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_last_not_configured_story/2). + +admin_get_last_not_configured_story(Config, Alice) -> + Res = admin_get_last(Alice, Config), + get_not_loaded(Res). + +admin_count_active_users_last_not_configured(Config) -> + Domain = domain_helper:domain(), + Res = admin_count_active_users(Domain, null, Config), + get_not_loaded(Res). + +admin_remove_old_users_domain_last_not_configured(Config) -> + Domain = domain_helper:domain(), + Res = admin_remove_old_users(Domain, now_dt_with_offset(150), Config), + get_not_loaded(Res). + +admin_remove_old_users_global_last_not_configured(Config) -> + Res = admin_remove_old_users(null, now_dt_with_offset(150), Config), + get_ok_value([], Res). + +admin_list_old_users_domain_last_not_configured(Config) -> + Domain = domain_helper:domain(), + Res = admin_list_old_users(Domain, now_dt_with_offset(150), Config), + get_not_loaded(Res). + +admin_list_old_users_global_last_not_configured(Config) -> + Res = admin_list_old_users(null, now_dt_with_offset(150), Config), + get_ok_value([], Res). + +user_set_last_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_set_last_not_configured_story/2). + +user_set_last_not_configured_story(Config, Alice) -> + Status = <<"My first status">>, + Res = user_set_last(Alice, Status, ?DEFAULT_DT, Config), + get_not_loaded(Res). + +user_get_last_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_last_not_configured_story/2). +user_get_last_not_configured_story(Config, Alice) -> + Res = user_get_last(Alice, Alice, Config), + get_not_loaded(Res). + %% Helpers jids_with_config(Config, Users, Fun) -> From d1d98a7f5ea1936b51aeb6d4be9f41750124f6cf Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Tue, 20 Sep 2022 14:51:13 +0200 Subject: [PATCH 103/117] Adding annotations and tests to muc_light --- big_tests/tests/graphql_inbox_SUITE.erl | 9 +- big_tests/tests/graphql_muc_light_SUITE.erl | 302 ++++++++++++++++-- priv/graphql/schemas/admin/muc_light.gql | 28 +- priv/graphql/schemas/user/muc_light.gql | 26 +- ...goose_graphql_muc_light_admin_mutation.erl | 2 +- 5 files changed, 309 insertions(+), 58 deletions(-) diff --git a/big_tests/tests/graphql_inbox_SUITE.erl b/big_tests/tests/graphql_inbox_SUITE.erl index 135bc239ca2..011d81d35e6 100644 --- a/big_tests/tests/graphql_inbox_SUITE.erl +++ b/big_tests/tests/graphql_inbox_SUITE.erl @@ -113,8 +113,13 @@ ensure_inbox_started() -> ok = dynamic_modules:ensure_modules(HostType, Modules), ok = dynamic_modules:ensure_modules(SecHostType, Modules). -end_per_group(_, _) -> - graphql_helper:clean(). +end_per_group(Group, _Config) when Group =:= user; + Group =:= admin_http; + Group =:= domain_admin_inbox; + Group =:= admin_cli -> + graphql_helper:clean(); +end_per_group(_Group, _Config) -> + escalus_fresh:clean(). init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). diff --git a/big_tests/tests/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl index 6d50167f6b9..eabeff83f40 100644 --- a/big_tests/tests/graphql_muc_light_SUITE.erl +++ b/big_tests/tests/graphql_muc_light_SUITE.erl @@ -5,7 +5,8 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_listener_port/1, get_listener_config/1, get_ok_value/2, get_err_msg/1, - get_coercion_err_msg/1, make_creds/1, get_unauthorized/1]). + get_coercion_err_msg/1, make_creds/1, get_unauthorized/1, + get_err_code/1, get_not_loaded/1]). -import(config_parser_helper, [mod_config/2]). @@ -36,16 +37,28 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_muc_light}, - {group, admin_muc_light_http}, - {group, admin_muc_light_cli}, + [{group, user}, + {group, admin_http}, + {group, admin_cli}, {group, domain_admin_muc_light}]. groups() -> - [{user_muc_light, [parallel], user_muc_light_tests()}, - {admin_muc_light_http, [parallel], admin_muc_light_tests()}, - {admin_muc_light_cli, [], admin_muc_light_tests()}, - {domain_admin_muc_light, [], domain_admin_muc_light_tests()}]. + [{user, [], user_groups()}, + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {user_muc_light, [parallel], user_muc_light_tests()}, + {user_muc_light_not_configured, [], user_muc_light_not_configured_tests()}, + {admin_muc_light, [parallel], admin_muc_light_tests()}, + {domain_admin_muc_light, [], domain_admin_muc_light_tests()}, + {admin_muc_light_not_configured, [], admin_muc_light_not_configured_tests()}]. + +user_groups() -> + [{group, user_muc_light}, + {group, user_muc_light_not_configured}]. + +admin_groups() -> + [{group, admin_muc_light}, + {group, admin_muc_light_not_configured}]. user_muc_light_tests() -> [user_create_room, @@ -66,6 +79,19 @@ user_muc_light_tests() -> user_blocking_list ]. +user_muc_light_not_configured_tests() -> + [user_create_room_muc_light_not_configured, + user_change_room_config_muc_light_not_configured, + user_invite_user_muc_light_not_configured, + user_delete_room_muc_light_not_configured, + user_kick_user_muc_light_not_configured, + user_send_message_to_room_muc_light_not_configured, + user_get_room_messages_muc_light_not_configured, + user_list_rooms_muc_light_not_configured, + user_list_room_users_muc_light_not_configured, + user_get_room_config_muc_light_not_configured, + user_blocking_list_muc_light_not_configured]. + admin_muc_light_tests() -> [admin_create_room, admin_create_room_with_custom_fields, @@ -125,28 +151,31 @@ domain_admin_muc_light_tests() -> domain_admin_blocking_list_no_permission ]. +admin_muc_light_not_configured_tests() -> + [admin_create_room_muc_light_not_configured, + admin_change_room_config_muc_light_not_configured, + admin_invite_user_muc_light_not_configured, + admin_delete_room_muc_light_not_configured, + admin_kick_user_muc_light_not_configured, + admin_send_message_to_room_muc_light_not_configured, + admin_get_room_messages_muc_light_not_configured, + admin_list_user_rooms_muc_light_not_configured, + admin_list_room_users_muc_light_not_configured, + admin_get_room_config_muc_light_not_configured, + admin_blocking_list_muc_light_not_configured]. + init_per_suite(Config) -> - Config1 = init_modules(Config), - Config2 = ejabberd_node_utils:init(mim(), Config1), - [{muc_light_host, muc_light_helper:muc_host()}, - {secondary_muc_light_host, <<"muclight.", (domain_helper:secondary_domain())/binary>>} - | escalus:init_per_suite(Config2)]. + HostType = domain_helper:host_type(), + Config1 = dynamic_modules:save_modules(HostType, Config), + Config2 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config1), + Config3 = ejabberd_node_utils:init(mim(), Config2), + escalus:init_per_suite(Config3). end_per_suite(Config) -> escalus_fresh:clean(), dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_modules(Config) -> - HostType = domain_helper:host_type(), - SecondaryHostType = domain_helper:secondary_host_type(), - Config1 = dynamic_modules:save_modules(HostType, Config), - Config2 = dynamic_modules:save_modules(SecondaryHostType, Config1), - Config3 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config2), - dynamic_modules:ensure_modules(HostType, required_modules(suite)), - dynamic_modules:ensure_modules(SecondaryHostType, required_modules(suite)), - Config3. - required_modules(_) -> MucLightOpts = mod_config(mod_muc_light, #{rooms_in_rosters => true, config_schema => custom_schema()}), @@ -160,17 +189,38 @@ custom_schema() -> {<<"roomname">>, <<>>, roomname, binary}, {<<"subject">>, <<>>, subject, binary}]. -init_per_group(admin_muc_light_http, Config) -> +init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); -init_per_group(admin_muc_light_cli, Config) -> +init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin_muc_light, Config) -> + ensure_muc_light_started(Config), graphql_helper:init_domain_admin_handler(Config); -init_per_group(user_muc_light, Config) -> +init_per_group(Group, Config) when Group =:= user_muc_light; + Group =:= admin_muc_light -> + ensure_muc_light_started(Config); +init_per_group(Group, Config) when Group =:= user_muc_light_not_configured; + Group =:= admin_muc_light_not_configured -> + ensure_muc_light_stopped(Config); +init_per_group(user, Config) -> graphql_helper:init_user(Config). -end_per_group(_GN, _Config) -> - graphql_helper:clean(). +ensure_muc_light_started(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:ensure_modules(HostType, required_modules(suite)), + [{muc_light_host, muc_light_helper:muc_host()} | Config]. + +ensure_muc_light_stopped(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:ensure_modules(HostType, [{mod_muc_light, stopped}]), + [{muc_light_host, <<"NON_EXISTING">>} | Config]. + +end_per_group(Group, _Config) when Group =:= user; + Group =:= admin_http; + Group =:= admin_cli -> + graphql_helper:clean(); +end_per_group(_Group, _Config) -> + escalus_fresh:clean(). init_per_testcase(TC, Config) -> rest_helper:maybe_skip_mam_test_cases(TC, [user_get_room_messages, @@ -1233,8 +1283,204 @@ admin_get_room_config_non_existent_domain_story(Config, Alice) -> Res = get_room_config(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). +%% User mod_muc_light not configured test cases +user_create_room_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_create_room_muc_light_not_configured_story/2). + +user_create_room_muc_light_not_configured_story(Config, Alice) -> + MucServer = ?config(muc_light_host, Config), + Name = <<"first room">>, + Subject = <<"testing">>, + Res = user_create_room(Alice, MucServer, Name, Subject, null, Config), + ?assertEqual(<<"muc_server_not_found">>, get_err_code(Res)). + +user_change_room_config_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_change_room_config_muc_light_not_configured_story/2). + +user_change_room_config_muc_light_not_configured_story(Config, Alice) -> + Name = <<"changed room">>, + Subject = <<"not testing">>, + Res = user_change_room_configuration(Alice, get_room_name(), Name, Subject, Config), + get_not_loaded(Res). + +user_invite_user_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_invite_user_muc_light_not_configured_story/3). + +user_invite_user_muc_light_not_configured_story(Config, Alice, Bob) -> + BobBin = escalus_client:short_jid(Bob), + Res = user_invite_user(Alice, get_room_name(), BobBin, Config), + get_not_loaded(Res). + +user_delete_room_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_delete_room_muc_light_not_configured_story/2). + +user_delete_room_muc_light_not_configured_story(Config, Alice) -> + Res = user_delete_room(Alice, get_room_name(), Config), + get_not_loaded(Res). + +user_kick_user_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_kick_user_muc_light_not_configured_story/2). + +user_kick_user_muc_light_not_configured_story(Config, Alice) -> + Res = user_kick_user(Alice, get_room_name(), null, Config), + get_not_loaded(Res). + +user_send_message_to_room_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_send_message_to_room_muc_light_not_configured_story/2). + +user_send_message_to_room_muc_light_not_configured_story(Config, Alice) -> + MsgBody = <<"Hello there!">>, + Res = user_send_message_to_room(Alice, get_room_name(), MsgBody, Config), + get_not_loaded(Res). + +user_get_room_messages_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_room_messages_muc_light_not_configured_story/2). + +user_get_room_messages_muc_light_not_configured_story(Config, Alice) -> + Before = <<"2022-02-17T04:54:13+00:00">>, + Res = user_get_room_messages(Alice, get_room_name(), 51, Before, Config), + get_not_loaded(Res). + +user_list_rooms_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_list_rooms_muc_light_not_configured_story/2). + +user_list_rooms_muc_light_not_configured_story(Config, Alice) -> + Res = user_list_rooms(Alice, Config), + get_not_loaded(Res). + +user_list_room_users_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_list_room_users_muc_light_not_configured_story/2). + +user_list_room_users_muc_light_not_configured_story(Config, Alice) -> + Res = user_list_room_users(Alice, get_room_name(), Config), + get_not_loaded(Res). + +user_get_room_config_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_room_config_muc_light_not_configured_story/2). + +user_get_room_config_muc_light_not_configured_story(Config, Alice) -> + Res = user_get_room_config(Alice, get_room_name(), Config), + get_not_loaded(Res). + +user_blocking_list_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_blocking_list_muc_light_not_configured_story/3). + +user_blocking_list_muc_light_not_configured_story(Config, Alice, Bob) -> + BobBin = escalus_client:full_jid(Bob), + Res = user_get_blocking(Alice, Config), + get_not_loaded(Res), + Res2 = user_set_blocking(Alice, [{<<"USER">>, <<"DENY">>, BobBin}], Config), + get_not_loaded(Res2). + +%% Admin mod_muc_light not configured test cases + +admin_create_room_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_create_room_muc_light_not_configured_story/2). + +admin_create_room_muc_light_not_configured_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MucServer = ?config(muc_light_host, Config), + Name = <<"first room">>, + Subject = <<"testing">>, + Res = create_room(MucServer, Name, AliceBin, Subject, null, Config), + ?assertEqual(<<"muc_server_not_found">>, get_err_code(Res)). + +admin_invite_user_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_invite_user_muc_light_not_configured_story/3). + +admin_invite_user_muc_light_not_configured_story(Config, Alice, Bob) -> + AliceBin = escalus_client:short_jid(Alice), + BobBin = escalus_client:short_jid(Bob), + Res = invite_user(get_room_name(), AliceBin, BobBin, Config), + get_not_loaded(Res). + +admin_change_room_config_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_change_room_config_muc_light_not_configured_story/2). + +admin_change_room_config_muc_light_not_configured_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Name = <<"changed room">>, + Subject = <<"not testing">>, + Res = change_room_configuration(get_room_name(), AliceBin, Name, Subject, Config), + get_not_loaded(Res). + +admin_delete_room_muc_light_not_configured(Config) -> + Res = delete_room(get_room_name(), Config), + get_not_loaded(Res). + +admin_kick_user_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{bob, 1}], + fun admin_kick_user_muc_light_not_configured_story/2). + +admin_kick_user_muc_light_not_configured_story(Config, Bob) -> + BobBin = escalus_client:short_jid(Bob), + Res = kick_user(get_room_name(), BobBin, Config), + get_not_loaded(Res). + +admin_send_message_to_room_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_send_message_to_room_muc_light_not_configured_story/2). + +admin_send_message_to_room_muc_light_not_configured_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MsgBody = <<"Hello there!">>, + Res = send_message_to_room(get_room_name(), AliceBin, MsgBody, Config), + get_not_loaded(Res). + +admin_get_room_messages_muc_light_not_configured(Config) -> + Limit = 40, + Res = get_room_messages(get_room_name(), Limit, null, Config), + get_not_loaded(Res). + +admin_list_user_rooms_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_list_user_rooms_muc_light_not_configured_story/2). + +admin_list_user_rooms_muc_light_not_configured_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Res = list_user_rooms(AliceBin, Config), + get_not_loaded(Res). + +admin_list_room_users_muc_light_not_configured(Config) -> + Res = list_room_users(get_room_name(), Config), + get_not_loaded(Res). + +admin_get_room_config_muc_light_not_configured(Config) -> + Res = get_room_config(get_room_name(), Config), + get_not_loaded(Res). + %% Helpers +get_room_name() -> + Domain = domain_helper:domain(), + <<"NON_EXISTING@", Domain/binary>>. + +admin_blocking_list_muc_light_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_blocking_list_muc_light_not_configured_story/3). + +admin_blocking_list_muc_light_not_configured_story(Config, Alice, Bob) -> + AliceBin = escalus_client:full_jid(Alice), + BobBin = escalus_client:full_jid(Bob), + Res = get_user_blocking(AliceBin, Config), + get_not_loaded(Res), + Res2 = set_blocking(AliceBin, [{<<"USER">>, <<"DENY">>, BobBin}], Config), + get_not_loaded(Res2). + make_bare_jid(User, Server) -> JID = jid:make_bare(User, Server), jid:to_binary(JID). diff --git a/priv/graphql/schemas/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql index 26f0bbda88b..87879b5c1b6 100644 --- a/priv/graphql/schemas/admin/muc_light.gql +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -1,47 +1,47 @@ """ Allow admin to manage Multi-User Chat Light rooms. """ -type MUCLightAdminMutation @protected{ +type MUCLightAdminMutation @protected @use(modules: ["mod_muc_light"]){ "Create a MUC light room under the given XMPP hostname" createRoom(mucDomain: String!, name: String!, owner: JID!, subject: String!, id: NonEmptyString, options: [RoomConfigDictEntryInput!]): Room - @protected(type: DOMAIN, args: ["owner"]) + @protected(type: DOMAIN, arg: "owner") "Change configuration of a MUC Light room" changeRoomConfiguration(room: JID!, owner: JID!, name: String!, subject: String!, options: [RoomConfigDictEntryInput!]): Room - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Invite a user to a MUC Light room" inviteUser(room: JID!, sender: JID!, recipient: JID!): String - @protected(type: DOMAIN, args: ["sender"]) + @protected(type: DOMAIN, args: ["sender"]) @use(arg: "room") "Remove a MUC Light room" deleteRoom(room: JID!): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Kick a user from a MUC Light room" kickUser(room: JID!, user: JID!): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Send a message to a MUC Light room" sendMessageToRoom(room: JID!, from: JID!, body: String!): String - @protected(type: DOMAIN, args: ["from"]) + @protected(type: DOMAIN, args: ["from"]) @use(arg: "room") "Set the user's list of blocked entities" setBlockingList(user: JID!, items: [BlockingInput!]!): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } """ Allow admin to get information about Multi-User Chat Light rooms. """ -type MUCLightAdminQuery @protected{ +type MUCLightAdminQuery @protected @use(modules: ["mod_muc_light"]){ "Get the MUC Light room archived messages" getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get configuration of the MUC Light room" getRoomConfig(room: JID!): Room - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get users list of given MUC Light room" listRoomUsers(room: JID!): [RoomUser!] - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get the list of MUC Light rooms that the user participates in" listUserRooms(user: JID!): [JID!] - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") "Get the user's list of blocked entities" getBlockingList(user: JID!): [BlockingItem!] - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } diff --git a/priv/graphql/schemas/user/muc_light.gql b/priv/graphql/schemas/user/muc_light.gql index 89f74b062b0..108363a18c1 100644 --- a/priv/graphql/schemas/user/muc_light.gql +++ b/priv/graphql/schemas/user/muc_light.gql @@ -1,35 +1,35 @@ """ Allow user to manage Multi-User Chat Light rooms. """ -type MUCLightUserMutation @protected{ +type MUCLightUserMutation @protected @use(modules: ["mod_muc_light"]){ "Create a MUC light room under the given XMPP hostname" createRoom(mucDomain: String!, name: String!, subject: String!, id: NonEmptyString, options: [RoomConfigDictEntryInput!]): Room "Change configuration of a MUC Light room" - changeRoomConfiguration(room: JID!, name: String!, subject: String!, options: [RoomConfigDictEntryInput!]): Room + changeRoomConfiguration(room: JID!, name: String!, subject: String!, options: [RoomConfigDictEntryInput!]): Room @use(arg: "room") "Invite a user to a MUC Light room" - inviteUser(room: JID!, recipient: JID!): String + inviteUser(room: JID!, recipient: JID!): String @use(arg: "room") "Remove a MUC Light room" - deleteRoom(room: JID!): String + deleteRoom(room: JID!): String @use(arg: "room") "Kick a user from a MUC Light room" - kickUser(room: JID!, user: JID): String + kickUser(room: JID!, user: JID): String @use(arg: "room") "Send a message to a MUC Light room" - sendMessageToRoom(room: JID!, body: String!): String + sendMessageToRoom(room: JID!, body: String!): String @use(arg: "room") "Set the user blocking list" - setBlockingList(items: [BlockingInput!]!): String + setBlockingList(items: [BlockingInput!]!): String @use } """ Allow user to get information about Multi-User Chat Light rooms. """ -type MUCLightUserQuery @protected{ +type MUCLightUserQuery @protected @use(modules: ["mod_muc_light"]){ "Get the MUC Light room archived messages" - getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload + getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload @use(arg: "room") "Get configuration of the MUC Light room" - getRoomConfig(room: JID!): Room + getRoomConfig(room: JID!): Room @use(arg: "room") "Get users list of given MUC Light room" - listRoomUsers(room: JID!): [RoomUser!] + listRoomUsers(room: JID!): [RoomUser!] @use(arg: "room") "Get the list of MUC Light rooms that the user participates in" - listRooms: [JID!] + listRooms: [JID!] @use "Get the user blocking list" - getBlockingList: [BlockingItem!] + getBlockingList: [BlockingItem!] @use } diff --git a/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl index abe77a394a7..6a2cd7a36b4 100644 --- a/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl @@ -10,7 +10,7 @@ -import(mongoose_graphql_helper, [make_error/2, format_result/2]). -import(mongoose_graphql_muc_light_helper, [make_room/1, make_ok_user/1, prepare_blocking_items/1, - null_to_default/2, options_to_map/1]). + null_to_default/2, options_to_map/1, get_not_loaded/1]). execute(_Ctx, _Obj, <<"createRoom">>, Args) -> create_room(Args); From 2268b091a7a27d6a629efbd05e556d7d3f7c0cbc Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Thu, 22 Sep 2022 10:06:51 +0200 Subject: [PATCH 104/117] Fixing graphql_muc_light_SUITE tests --- big_tests/tests/graphql_muc_light_SUITE.erl | 21 +++++++++++++++------ priv/graphql/schemas/admin/muc_light.gql | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/big_tests/tests/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl index eabeff83f40..4b7d1930640 100644 --- a/big_tests/tests/graphql_muc_light_SUITE.erl +++ b/big_tests/tests/graphql_muc_light_SUITE.erl @@ -166,10 +166,12 @@ admin_muc_light_not_configured_tests() -> init_per_suite(Config) -> HostType = domain_helper:host_type(), + SecondaryHostType = domain_helper:secondary_host_type(), Config1 = dynamic_modules:save_modules(HostType, Config), - Config2 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config1), - Config3 = ejabberd_node_utils:init(mim(), Config2), - escalus:init_per_suite(Config3). + Config2 = dynamic_modules:save_modules(SecondaryHostType, Config1), + Config3 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config2), + Config4 = ejabberd_node_utils:init(mim(), Config3), + escalus:init_per_suite(Config4). end_per_suite(Config) -> escalus_fresh:clean(), @@ -194,8 +196,8 @@ init_per_group(admin_http, Config) -> init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin_muc_light, Config) -> - ensure_muc_light_started(Config), - graphql_helper:init_domain_admin_handler(Config); + Config1 = ensure_muc_light_started(Config), + graphql_helper:init_domain_admin_handler(Config1); init_per_group(Group, Config) when Group =:= user_muc_light; Group =:= admin_muc_light -> ensure_muc_light_started(Config); @@ -207,16 +209,23 @@ init_per_group(user, Config) -> ensure_muc_light_started(Config) -> HostType = domain_helper:host_type(), + SecondaryHostType = domain_helper:secondary_host_type(), dynamic_modules:ensure_modules(HostType, required_modules(suite)), - [{muc_light_host, muc_light_helper:muc_host()} | Config]. + dynamic_modules:ensure_modules(SecondaryHostType, required_modules(suite)), + [{muc_light_host, muc_light_helper:muc_host()}, + {secondary_muc_light_host, <<"muclight.", (domain_helper:secondary_domain())/binary>>} + | Config]. ensure_muc_light_stopped(Config) -> HostType = domain_helper:host_type(), + SecondaryHostType = domain_helper:secondary_host_type(), dynamic_modules:ensure_modules(HostType, [{mod_muc_light, stopped}]), + dynamic_modules:ensure_modules(SecondaryHostType, [{mod_muc_light, stopped}]), [{muc_light_host, <<"NON_EXISTING">>} | Config]. end_per_group(Group, _Config) when Group =:= user; Group =:= admin_http; + Group =:= domain_admin_muc_light; Group =:= admin_cli -> graphql_helper:clean(); end_per_group(_Group, _Config) -> diff --git a/priv/graphql/schemas/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql index 87879b5c1b6..4ba7ab9979a 100644 --- a/priv/graphql/schemas/admin/muc_light.gql +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -1,10 +1,10 @@ """ Allow admin to manage Multi-User Chat Light rooms. """ -type MUCLightAdminMutation @protected @use(modules: ["mod_muc_light"]){ +type MUCLightAdminMutation @use(modules: ["mod_muc_light"]) @protected{ "Create a MUC light room under the given XMPP hostname" createRoom(mucDomain: String!, name: String!, owner: JID!, subject: String!, id: NonEmptyString, options: [RoomConfigDictEntryInput!]): Room - @protected(type: DOMAIN, arg: "owner") + @protected(type: DOMAIN, args: ["owner"]) "Change configuration of a MUC Light room" changeRoomConfiguration(room: JID!, owner: JID!, name: String!, subject: String!, options: [RoomConfigDictEntryInput!]): Room @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") From c59707b998391449743e5be7442bace02ccab0ba Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Thu, 22 Sep 2022 10:43:18 +0200 Subject: [PATCH 105/117] Fixing mongooseimctl_SUITE tests --- src/http_upload/mod_http_upload_api.erl | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/http_upload/mod_http_upload_api.erl b/src/http_upload/mod_http_upload_api.erl index 702f9f06339..e490d5993f5 100644 --- a/src/http_upload/mod_http_upload_api.erl +++ b/src/http_upload/mod_http_upload_api.erl @@ -34,13 +34,19 @@ get_urls(Domain, Filename, Size, ContentType, Timeout) -> end. check_module_and_get_urls(HostType, Filename, Size, ContentType, Timeout) -> - case mod_http_upload:get_urls(HostType, Filename, Size, ContentType, Timeout) of - {PutURL, GetURL, Header} -> - {ok, #{<<"PutUrl">> => PutURL, <<"GetUrl">> => GetURL, - <<"Header">> => header_output(Header)}}; - file_too_large_error -> - {file_too_large_error, - "Declared file size exceeds the host's maximum file size."} + %The check if the module is loaded is needed by one test in mongooseimctl_SUITE + case gen_mod:is_loaded(HostType, mod_http_upload) of + true -> + case mod_http_upload:get_urls(HostType, Filename, Size, ContentType, Timeout) of + {PutURL, GetURL, Header} -> + {ok, #{<<"PutUrl">> => PutURL, <<"GetUrl">> => GetURL, + <<"Header">> => header_output(Header)}}; + file_too_large_error -> + {file_too_large_error, + "Declared file size exceeds the host's maximum file size."} + end; + false -> + {module_not_loaded_error, "mod_http_upload is not loaded for this host"} end. -spec generate_output_message(PutURL :: binary(), GetURL :: binary(), From 769b0dea0c765e2d09f2ef935aaf8a57a3bbf966 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Mon, 26 Sep 2022 16:02:30 +0200 Subject: [PATCH 106/117] Adding @use annotations and tests in graphql_muc --- big_tests/tests/graphql_muc_SUITE.erl | 312 ++++++++++++++++++++++++-- big_tests/tests/muc_helper.erl | 4 + priv/graphql/schemas/admin/muc.gql | 32 +-- priv/graphql/schemas/user/muc.gql | 32 +-- 4 files changed, 332 insertions(+), 48 deletions(-) diff --git a/big_tests/tests/graphql_muc_SUITE.erl b/big_tests/tests/graphql_muc_SUITE.erl index a15b97051da..b5b88c97c9b 100644 --- a/big_tests/tests/graphql_muc_SUITE.erl +++ b/big_tests/tests/graphql_muc_SUITE.erl @@ -5,7 +5,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). -import(graphql_helper, [execute_command/4, execute_user_command/5, get_ok_value/2, get_err_msg/1, get_coercion_err_msg/1, user_to_bin/1, user_to_full_bin/1, user_to_jid/1, - get_unauthorized/1]). + get_unauthorized/1, get_not_loaded/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -15,17 +15,29 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_muc}, - {group, admin_muc_http}, - {group, admin_muc_cli}, + [{group, user}, + {group, admin_http}, + {group, admin_cli}, {group, domain_admin_muc}]. groups() -> - [{user_muc, [parallel], user_muc_tests()}, - {admin_muc_http, [parallel], admin_muc_tests()}, - {admin_muc_cli, [], admin_muc_tests()}, + [{user, [], user_groups()}, + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {admin_muc_configured, [], admin_muc_tests()}, + {admin_muc_not_configured, [], admin_muc_not_configured_tests()}, + {user_muc_not_configured, [parallel], user_muc_not_configured_tests()}, + {user_muc_configured, [parallel], user_muc_tests()}, {domain_admin_muc, [], domain_admin_muc_tests()}]. +user_groups() -> + [{group, user_muc_configured}, + {group, user_muc_not_configured}]. + +admin_groups() -> + [{group, admin_muc_configured}, + {group, admin_muc_not_configured}]. + user_muc_tests() -> [user_create_and_delete_room, user_try_delete_nonexistent_room, @@ -66,6 +78,22 @@ user_muc_tests() -> user_try_list_nonexistent_room_affiliations ]. +user_muc_not_configured_tests() -> + [user_delete_room_muc_not_configured, + user_list_room_users_muc_not_configured, + user_change_room_config_muc_not_configured, + user_get_room_config_muc_not_configured, + user_invite_user_muc_not_configured, + user_kick_user_muc_not_configured, + user_send_message_to_room_muc_not_configured, + user_send_private_message_muc_not_configured, + user_get_room_messages_muc_not_configured, + user_owner_set_user_affiliation_muc_not_configured, + user_moderator_set_user_role_muc_not_configured, + user_can_enter_room_muc_not_configured, + user_can_exit_room_muc_not_configured, + user_list_room_affiliation_muc_not_configured]. + admin_muc_tests() -> [admin_create_and_delete_room, admin_try_create_instant_room_with_nonexistent_domain, @@ -104,6 +132,22 @@ admin_muc_tests() -> admin_try_list_nonexistent_room_affiliations ]. +admin_muc_not_configured_tests() -> + [admin_delete_room_muc_not_configured, + admin_list_room_users_muc_not_configured, + admin_change_room_config_muc_not_configured, + admin_get_room_config_muc_not_configured, + admin_invite_user_muc_not_configured, + admin_kick_user_muc_not_configured, + admin_send_message_to_room_muc_not_configured, + admin_send_private_message_muc_not_configured, + admin_get_room_messages_muc_not_configured, + admin_set_user_affiliation_muc_not_configured, + admin_set_user_role_muc_not_configured, + admin_make_user_enter_room_muc_not_configured, + admin_make_user_exit_room_muc_not_configured, + admin_list_room_affiliations_muc_not_configured]. + domain_admin_muc_tests() -> [admin_create_and_delete_room, admin_try_create_instant_room_with_nonexistent_domain, @@ -157,9 +201,6 @@ init_per_suite(Config) -> Config6 = ejabberd_node_utils:init(mim(), Config5), dynamic_modules:restart(HostType, mod_disco, config_parser_helper:default_mod_config(mod_disco)), - muc_helper:load_muc(), - muc_helper:load_muc(SecondaryHostType), - mongoose_helper:ensure_muc_clean(), Config6. end_per_suite(Config) -> @@ -169,17 +210,42 @@ end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_per_group(admin_muc_http, Config) -> +init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); -init_per_group(admin_muc_cli, Config) -> +init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin_muc, Config) -> - graphql_helper:init_domain_admin_handler(Config); -init_per_group(user_muc, Config) -> + Config1 = ensure_muc_started(Config), + graphql_helper:init_domain_admin_handler(Config1); +init_per_group(Group, Config) when Group =:= admin_muc_configured; + Group =:= user_muc_configured -> + ensure_muc_started(Config); +init_per_group(Group, Config) when Group =:= admin_muc_not_configured; + Group =:= user_muc_not_configured -> + ensure_muc_stopped(Config); +init_per_group(user, Config) -> graphql_helper:init_user(Config). -end_per_group(_GN, _Config) -> - graphql_helper:clean(). +ensure_muc_started(Config) -> + SecondaryHostType = domain_helper:secondary_host_type(), + muc_helper:load_muc(), + muc_helper:load_muc(SecondaryHostType), + mongoose_helper:ensure_muc_clean(), + Config. + +ensure_muc_stopped(Config) -> + SecondaryHostType = domain_helper:secondary_host_type(), + muc_helper:unload_muc(), + muc_helper:unload_muc(SecondaryHostType), + Config. + +end_per_group(Group, _Config) when Group =:= user; + Group =:= admin_http; + Group =:= domain_admin_muc; + Group =:= admin_cli -> + graphql_helper:clean(); +end_per_group(_Group, _Config) -> + escalus_fresh:clean(). init_per_testcase(TC, Config) -> rest_helper:maybe_skip_mam_test_cases(TC, [user_get_room_messages, @@ -634,6 +700,96 @@ admin_try_list_nonexistent_room_affiliations(Config) -> Res = list_room_affiliations(?NONEXISTENT_ROOM, null, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). +%% Admin MUC not configured test cases + +admin_delete_room_muc_not_configured(Config) -> + Res = delete_room(get_room_name(), null, Config), + get_not_loaded(Res). + +admin_list_room_users_muc_not_configured(Config) -> + Res = list_room_users(get_room_name(), Config), + get_not_loaded(Res). + +admin_change_room_config_muc_not_configured(Config) -> + RoomConfig = #{title => <<"NewTitle">>}, + Res = change_room_config(get_room_name(), RoomConfig, Config), + get_not_loaded(Res). + +admin_get_room_config_muc_not_configured(Config) -> + Res = get_room_config(get_room_name(), Config), + get_not_loaded(Res). + +admin_invite_user_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_invite_user_muc_not_configured_story/3). + +admin_invite_user_muc_not_configured_story(Config, Alice, Bob) -> + Res = invite_user(get_room_name(), Alice, Bob, null, Config), + get_not_loaded(Res). + +admin_kick_user_muc_not_configured(Config) -> + Res = kick_user(get_room_name(), <<"nick">>, <<"reason">>, Config), + get_not_loaded(Res). + +admin_send_message_to_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_send_message_to_room_muc_not_configured_story/2). + +admin_send_message_to_room_muc_not_configured_story(Config, Alice) -> + Res = send_message_to_room(get_room_name(), Alice, <<"body">>, Config), + get_not_loaded(Res). + +admin_send_private_message_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_send_private_message_muc_not_configured_story/3). + +admin_send_private_message_muc_not_configured_story(Config, Alice, Bob) -> + Nick = <<"ali">>, + Res = send_private_message(get_room_name(), Alice, Nick, <<"body">>, Config), + get_not_loaded(Res). + +admin_get_room_messages_muc_not_configured(Config) -> + Res = get_room_messages(get_room_name(), 4, null, Config), + get_not_loaded(Res). + +admin_set_user_affiliation_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_set_user_affiliation_muc_not_configured_story/2). + +admin_set_user_affiliation_muc_not_configured_story(Config, Alice) -> + Res = set_user_affiliation(get_room_name(), Alice, member, Config), + get_not_loaded(Res). + +admin_set_user_role_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_set_user_role_muc_not_configured_story/2). + +admin_set_user_role_muc_not_configured_story(Config, Alice) -> + Res = set_user_role(get_room_name(), Alice, moderator, Config), + get_not_loaded(Res). + +admin_make_user_enter_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_make_user_enter_room_muc_light_not_configured_story/2). + +admin_make_user_enter_room_muc_light_not_configured_story(Config, Alice) -> + Nick = <<"ali">>, + Res = enter_room(get_room_name(), Alice, Nick, null, Config), + get_not_loaded(Res). + +admin_make_user_exit_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_make_user_exit_room_muc_not_configured_story/2). + +admin_make_user_exit_room_muc_not_configured_story(Config, Alice) -> + Nick = <<"ali">>, + Res = exit_room(get_room_name(), Alice, Nick, Config), + get_not_loaded(Res). + +admin_list_room_affiliations_muc_not_configured(Config) -> + Res = list_room_affiliations(get_room_name(), member, Config), + get_not_loaded(Res). + %% Domain admin test cases domain_admin_try_delete_room_with_nonexistent_domain(Config) -> @@ -1342,6 +1498,126 @@ user_try_list_nonexistent_room_affiliations(Config, Alice) -> Res = user_list_room_affiliations(Alice, ?NONEXISTENT_ROOM, null, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)). +%% User MUC not configured test cases + +user_delete_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_delete_room_muc_not_configured_story/2). + +user_delete_room_muc_not_configured_story(Config, Alice) -> + Res = user_delete_room(Alice, get_room_name(), null, Config), + get_not_loaded(Res). + +user_list_room_users_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_list_room_users_muc_not_configured_story/2). + +user_list_room_users_muc_not_configured_story(Config, Alice) -> + Res = user_list_room_users(Alice, get_room_name(), Config), + get_not_loaded(Res). + +user_change_room_config_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_change_room_config_muc_not_configured_story/2). + +user_change_room_config_muc_not_configured_story(Config, Alice) -> + RoomConfig = #{title => <<"NewTitle">>}, + Res = user_change_room_config(Alice, get_room_name(), RoomConfig, Config), + get_not_loaded(Res). + +user_get_room_config_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_room_config_muc_not_configured_story/2). + +user_get_room_config_muc_not_configured_story(Config, Alice) -> + Res = user_get_room_config(Alice, get_room_name(), Config), + get_not_loaded(Res). + +user_invite_user_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_invite_user_muc_not_configured_story/3). + +user_invite_user_muc_not_configured_story(Config, Alice, Bob) -> + Res = user_invite_user(Alice, get_room_name(), Bob, null, Config), + get_not_loaded(Res). + +user_kick_user_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_kick_user_muc_not_configured_story/2). + +user_kick_user_muc_not_configured_story(Config, Alice) -> + Res = user_kick_user(Alice, get_room_name(), <<"nick">>, <<"reason">>, Config), + get_not_loaded(Res). + +user_send_message_to_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_send_message_to_room_muc_not_configured_story/2). + +user_send_message_to_room_muc_not_configured_story(Config, Alice) -> + Res = user_send_message_to_room(Alice, get_room_name(), <<"Body">>, null, Config), + get_not_loaded(Res). + +user_send_private_message_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_send_private_message_muc_not_configured_story/2). + +user_send_private_message_muc_not_configured_story(Config, Alice) -> + Message = <<"Hello Bob!">>, + BobNick = <<"Bobek">>, + Res = user_send_private_message(Alice, get_room_name(), Message, BobNick, null, Config), + get_not_loaded(Res). + +user_get_room_messages_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_room_messages_muc_not_configured_story/2). + +user_get_room_messages_muc_not_configured_story(Config, Alice) -> + Res = user_get_room_messages(Alice, get_room_name(), 10, null, Config), + get_not_loaded(Res). + +user_owner_set_user_affiliation_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_owner_set_user_affiliation_muc_not_configured_story/2). + +user_owner_set_user_affiliation_muc_not_configured_story(Config, Alice) -> + Res = user_set_user_affiliation(Alice, get_room_name(), <<"ali">>, member, Config), + get_not_loaded(Res). + +user_moderator_set_user_role_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_moderator_set_user_role_muc_not_configured_story/2). + +user_moderator_set_user_role_muc_not_configured_story(Config, Alice) -> + Res = user_set_user_role(Alice, get_room_name(), Alice, moderator, Config), + get_not_loaded(Res). + +user_can_enter_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_can_enter_room_muc_not_configured_story/2). + +user_can_enter_room_muc_not_configured_story(Config, Alice) -> + Nick = <<"ali">>, + Resource = escalus_client:resource(Alice), + Res = user_enter_room(Alice, get_room_name(), Nick, Resource, null, Config), + get_not_loaded(Res). + +user_can_exit_room_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_can_exit_room_muc_not_configured_story/2). + +user_can_exit_room_muc_not_configured_story(Config, Alice) -> + Resource = escalus_client:resource(Alice), + Res = user_exit_room(Alice, get_room_name(), <<"ali">>, Resource, Config), + get_not_loaded(Res). + +user_list_room_affiliation_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_list_room_affiliation_muc_not_configured_story/2). + +user_list_room_affiliation_muc_not_configured_story(Config, Alice) -> + Res = user_list_room_affiliations(Alice, get_room_name(), owner, Config), + get_not_loaded(Res). + %% Helpers assert_no_full_jid(Res) -> @@ -1429,6 +1705,10 @@ assert_default_room_config(Response) -> atom_to_enum_item(null) -> null; atom_to_enum_item(Atom) -> list_to_binary(string:to_upper(atom_to_list(Atom))). +get_room_name() -> + Domain = domain_helper:domain(), + <<"NON_EXISTING@", Domain/binary>>. + %% Commands create_instant_room(MUCDomain, Name, Owner, Nick, Config) -> diff --git a/big_tests/tests/muc_helper.erl b/big_tests/tests/muc_helper.erl index d2d7a02abe5..75c757b1db2 100644 --- a/big_tests/tests/muc_helper.erl +++ b/big_tests/tests/muc_helper.erl @@ -73,6 +73,10 @@ unload_muc() -> dynamic_modules:stop(HostType, mod_muc), dynamic_modules:stop(HostType, mod_muc_log). +unload_muc(HostType) -> + dynamic_modules:stop(HostType, mod_muc), + dynamic_modules:stop(HostType, mod_muc_log). + muc_host() -> ct:get_config({hosts, mim, muc_service}). diff --git a/priv/graphql/schemas/admin/muc.gql b/priv/graphql/schemas/admin/muc.gql index 53763c2784b..9fc23919977 100644 --- a/priv/graphql/schemas/admin/muc.gql +++ b/priv/graphql/schemas/admin/muc.gql @@ -1,59 +1,59 @@ """ Allow admin to manage Multi-User Chat rooms. """ -type MUCAdminMutation @protected{ +type MUCAdminMutation @protected @use(modules: ["mod_muc"]){ "Create a MUC room under the given XMPP hostname" createInstantRoom(mucDomain: String!, name: String!, owner: JID!, nick: String!): MUCRoomDesc @protected(type: DOMAIN, args: ["owner"]) "Invite a user to a MUC room" inviteUser(room: JID!, sender: JID!, recipient: JID!, reason: String): String - @protected(type: DOMAIN, args: ["sender"]) + @protected(type: DOMAIN, args: ["sender"]) @use(arg: "room") "Kick a user from a MUC room" kickUser(room: JID!, nick: String!, reason: String): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Send a message to a MUC room" sendMessageToRoom(room: JID!, from: FullJID!, body: String!): String - @protected(type: DOMAIN, args: ["from"]) + @protected(type: DOMAIN, args: ["from"]) @use(arg: "room") "Send a private message to a MUC room user" sendPrivateMessage(room: JID!, from: FullJID!, toNick: String!, body: String!): String - @protected(type: DOMAIN, args: ["from"]) + @protected(type: DOMAIN, args: ["from"]) @use(arg: "room") "Remove a MUC room" deleteRoom(room: JID!, reason: String): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Change configuration of a MUC room" changeRoomConfiguration(room: JID!, config: MUCRoomConfigInput!): MUCRoomConfig - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Change a user role" setUserRole(room: JID!, nick: String!, role: MUCRole!): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Change a user affiliation" setUserAffiliation(room: JID!, user: JID!, affiliation: MUCAffiliation!): String - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Make a user enter the room with a given nick" enterRoom(room: JID!, user: FullJID!, nick: String!, password: String): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "room") "Make a user with the given nick exit the room" exitRoom(room: JID!, user: FullJID!, nick: String!): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "room") } """ Allow admin to get information about Multi-User Chat rooms. """ -type MUCAdminQuery @protected{ +type MUCAdminQuery @protected @use(modules: ["mod_muc"]){ "Get MUC rooms under the given MUC domain" listRooms(mucDomain: String!, from: JID, limit: Int, index: Int): MUCRoomsPayload! @protected(type: DOMAIN, args: ["from"]) "Get configuration of the MUC room" getRoomConfig(room: JID!): MUCRoomConfig - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get the user list of a given MUC room" listRoomUsers(room: JID!): [MUCRoomUser!] - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get the affiliation list of given MUC room" listRoomAffiliations(room: JID!, affiliation: MUCAffiliation): [MUCRoomAffiliation!] - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") "Get the MUC room archived messages" getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload - @protected(type: DOMAIN, args: ["room"]) + @protected(type: DOMAIN, args: ["room"]) @use(arg: "room") } diff --git a/priv/graphql/schemas/user/muc.gql b/priv/graphql/schemas/user/muc.gql index 7ec7a4ef970..5eee155f9e4 100644 --- a/priv/graphql/schemas/user/muc.gql +++ b/priv/graphql/schemas/user/muc.gql @@ -1,43 +1,43 @@ """ Allow user to manage Multi-User Chat rooms. """ -type MUCUserMutation @protected{ +type MUCUserMutation @protected @use(modules: ["mod_muc"]){ "Create a MUC room under the given XMPP hostname" createInstantRoom(mucDomain: String!, name: String!, nick: String!): MUCRoomDesc "Invite a user to a MUC room" - inviteUser(room: JID!, recipient: JID!, reason: String): String + inviteUser(room: JID!, recipient: JID!, reason: String): String @use(arg: "room") "Kick a user from a MUC room" - kickUser(room: JID!, nick: String!, reason: String): String + kickUser(room: JID!, nick: String!, reason: String): String @use(arg: "room") "Send a message to a MUC room" - sendMessageToRoom(room: JID!, body: String!, resource: String): String + sendMessageToRoom(room: JID!, body: String!, resource: String): String @use(arg: "room") "Send a private message to a MUC room user from the given resource" - sendPrivateMessage(room: JID!, toNick: String!, body: String!, resource: String): String + sendPrivateMessage(room: JID!, toNick: String!, body: String!, resource: String): String @use(arg: "room") "Remove a MUC room" - deleteRoom(room: JID!, reason: String): String + deleteRoom(room: JID!, reason: String): String @use(arg: "room") "Change configuration of a MUC room" - changeRoomConfiguration(room: JID!, config: MUCRoomConfigInput!): MUCRoomConfig + changeRoomConfiguration(room: JID!, config: MUCRoomConfigInput!): MUCRoomConfig @use(arg: "room") "Change a user role" - setUserRole(room: JID!, nick: String!, role: MUCRole!): String + setUserRole(room: JID!, nick: String!, role: MUCRole!): String @use(arg: "room") "Change a user affiliation" - setUserAffiliation(room: JID!, user: JID!, affiliation: MUCAffiliation!): String + setUserAffiliation(room: JID!, user: JID!, affiliation: MUCAffiliation!): String @use(arg: "room") "Enter the room with given resource and nick" - enterRoom(room: JID!, nick: String!, resource: String!, password: String): String + enterRoom(room: JID!, nick: String!, resource: String!, password: String): String @use(arg: "room") "Exit the room with given resource and nick" - exitRoom(room: JID!, nick: String!, resource: String!): String + exitRoom(room: JID!, nick: String!, resource: String!): String @use(arg: "room") } """ Allow user to get information about Multi-User Chat rooms. """ -type MUCUserQuery @protected{ +type MUCUserQuery @protected @use(modules: ["mod_muc"]){ "Get MUC rooms under the given MUC domain" listRooms(mucDomain: String!, limit: Int, index: Int): MUCRoomsPayload! "Get configuration of the MUC room" - getRoomConfig(room: JID!): MUCRoomConfig + getRoomConfig(room: JID!): MUCRoomConfig @use(arg: "room") "Get the user list of a given MUC room" - listRoomUsers(room: JID!): [MUCRoomUser!] + listRoomUsers(room: JID!): [MUCRoomUser!] @use(arg: "room") "Get the affiliation list of given MUC room" - listRoomAffiliations(room: JID!, affiliation: MUCAffiliation): [MUCRoomAffiliation!] + listRoomAffiliations(room: JID!, affiliation: MUCAffiliation): [MUCRoomAffiliation!] @use(arg: "room") "Get the MUC room archived messages" - getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload + getRoomMessages(room: JID!, pageSize: Int, before: DateTime): StanzasPayload @use(arg: "room") } From 41a5ffc3e4b5637d06ba6223334dace44a7f52c9 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Mon, 26 Sep 2022 16:25:12 +0200 Subject: [PATCH 107/117] Adding @use annotation in graphql_offline --- big_tests/tests/graphql_offline_SUITE.erl | 6 +++--- priv/graphql/schemas/admin/offline.gql | 6 +++--- src/offline/mod_offline_api.erl | 11 +++-------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/big_tests/tests/graphql_offline_SUITE.erl b/big_tests/tests/graphql_offline_SUITE.erl index f012dc96e07..def5d6253b0 100644 --- a/big_tests/tests/graphql_offline_SUITE.erl +++ b/big_tests/tests/graphql_offline_SUITE.erl @@ -5,7 +5,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(domain_helper, [host_type/0, domain/0]). -import(graphql_helper, [execute_command/4, get_ok_value/2, get_err_code/1, user_to_bin/1, - get_unauthorized/1]). + get_unauthorized/1, get_not_loaded/1]). -import(config_parser_helper, [mod_config/2]). -import(mongooseimctl_helper, [mongooseimctl/3, rpc_call/3]). @@ -157,11 +157,11 @@ admin_delete_old_messages_no_domain_test(Config) -> admin_delete_expired_messages_offline_not_configured_test(Config) -> Result = delete_expired_messages(domain(), Config), - ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)). + get_not_loaded(Result). admin_delete_old_messages_offline_not_configured_test(Config) -> Result = delete_old_messages(domain(), 2, Config), - ?assertEqual(<<"module_not_loaded_error">>, get_err_code(Result)). + get_not_loaded(Result). %% Domain admin test cases diff --git a/priv/graphql/schemas/admin/offline.gql b/priv/graphql/schemas/admin/offline.gql index fc4c7238627..82eeae7500c 100644 --- a/priv/graphql/schemas/admin/offline.gql +++ b/priv/graphql/schemas/admin/offline.gql @@ -1,11 +1,11 @@ """ Allow admin to delete offline messages from specified domain """ -type OfflineAdminMutation @protected{ +type OjflineAdminMutation @protected @use(modules: ["mod_offline"]){ "Delete offline messages whose date has expired" - deleteExpiredMessages(domain: String!): String + deleteExpiredMessages(domain: String!): String @use(arg: "domain") @protected(type: DOMAIN, args: ["domain"]) "Delete messages at least as old as the number of days specified in the parameter" deleteOldMessages(domain: String!, days: Int!): String - @protected(type: DOMAIN, args: ["domain"]) + @protected(type: DOMAIN, args: ["domain"]) @use(arg: "domain") } diff --git a/src/offline/mod_offline_api.erl b/src/offline/mod_offline_api.erl index 7ea21bac8f8..9c76fc5ee1d 100644 --- a/src/offline/mod_offline_api.erl +++ b/src/offline/mod_offline_api.erl @@ -3,24 +3,19 @@ -export([delete_expired_messages/1, delete_old_messages/2]). -spec delete_expired_messages(jid:lserver()) -> - {ok | domain_not_found | server_error | module_not_loaded_error, iolist()}. + {ok | domain_not_found | server_error, iolist()}. delete_expired_messages(Domain) -> call_for_loaded_module(Domain, fun remove_expired_messages/2, {Domain}). -spec delete_old_messages(jid:lserver(), Days :: integer()) -> - {ok | domain_not_found | server_error | module_not_loaded_error, iolist()}. + {ok | domain_not_found | server_error, iolist()}. delete_old_messages(Domain, Days) -> call_for_loaded_module(Domain, fun remove_old_messages/2, {Domain, Days}). call_for_loaded_module(Domain, Function, Args) -> case mongoose_domain_api:get_domain_host_type(Domain) of {ok, HostType} -> - case gen_mod:is_loaded(HostType, mod_offline) of - true -> - Function(Args, HostType); - false -> - {module_not_loaded_error, "mod_offline is not loaded for this host"} - end; + Function(Args, HostType); {error, not_found} -> {domain_not_found, "Unknown domain"} end. From 244f6f7322f944fafdf16cb4d1d8c6984ff168ce Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Tue, 27 Sep 2022 09:41:30 +0200 Subject: [PATCH 108/117] Adding @use directive and new tests in mod_private --- big_tests/tests/graphql_private_SUITE.erl | 111 ++++++++++++++++++---- priv/graphql/schemas/admin/private.gql | 8 +- priv/graphql/schemas/user/private.gql | 8 +- src/mod_private_api.erl | 20 +--- 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/big_tests/tests/graphql_private_SUITE.erl b/big_tests/tests/graphql_private_SUITE.erl index 42169f2e8a4..530d9a78d14 100644 --- a/big_tests/tests/graphql_private_SUITE.erl +++ b/big_tests/tests/graphql_private_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [mim/0, require_rpc_nodes/1]). -import(graphql_helper, [execute_user_command/5, execute_command/4, get_ok_value/2, get_err_code/1, - user_to_bin/1, get_unauthorized/1]). + user_to_bin/1, get_unauthorized/1, get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("exml/include/exml.hrl"). @@ -13,22 +13,38 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_private}, + [{group, user}, {group, domain_admin_private}, - {group, admin_private_http}, - {group, admin_private_cli}]. + {group, admin_http}, + {group, admin_cli}]. groups() -> - [{user_private, [], user_private_tests()}, + [{user, [], user_groups()}, {domain_admin_private, [], domain_admin_private_tests()}, - {admin_private_http, [], admin_private_tests()}, - {admin_private_cli, [], admin_private_tests()}]. + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {admin_private_configured, [], admin_private_tests()}, + {user_private_configured, [], user_private_tests()}, + {admin_private_not_configured, [], admin_private_not_configured_tests()}, + {user_private_not_configured, [], user_private_not_configured_tests()}]. + +admin_groups() -> + [{group, admin_private_configured}, + {group, admin_private_not_configured}]. + +user_groups() -> + [{group, user_private_configured}, + {group, user_private_not_configured}]. user_private_tests() -> [user_set_private, user_get_private, parse_xml_error]. +user_private_not_configured_tests() -> + [user_set_private_not_configured, + user_get_private_not_configured]. + domain_admin_private_tests() -> [admin_set_private, admin_get_private, @@ -41,13 +57,15 @@ admin_private_tests() -> no_user_error_set, no_user_error_get]. +admin_private_not_configured_tests() -> + [admin_set_private_not_configured, + admin_get_private_not_configured]. + init_per_suite(Config0) -> HostType = domain_helper:host_type(), Config1 = dynamic_modules:save_modules(HostType, Config0), Config2 = ejabberd_node_utils:init(mim(), Config1), Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), - ModConfig = create_config(Backend), - dynamic_modules:ensure_modules(HostType, ModConfig), escalus:init_per_suite([{backend, Backend} | Config2]). create_config(riak) -> @@ -61,18 +79,41 @@ end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_per_group(admin_private_http, Config) -> +init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); -init_per_group(admin_private_cli, Config) -> +init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); +init_per_group(user, Config) -> + graphql_helper:init_user(Config); init_per_group(domain_admin_private, Config) -> - graphql_helper:init_domain_admin_handler(Config); -init_per_group(user_private, Config) -> - graphql_helper:init_user(Config). + Config1 = ensure_private_started(Config), + graphql_helper:init_domain_admin_handler(Config1); +init_per_group(Group, Config) when Group =:= admin_private_configured; + Group =:= user_private_configured -> + ensure_private_started(Config); +init_per_group(Group, Config) when Group =:= admin_private_not_configured; + Group =:= user_private_not_configured -> + ensure_private_stopped(Config). + +ensure_private_started(Config) -> + HostType = domain_helper:host_type(), + Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), + ModConfig = create_config(Backend), + dynamic_modules:ensure_modules(HostType, ModConfig), + Config. +ensure_private_stopped(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:ensure_modules(HostType, [{mod_private, stopped}]), + Config. + +end_per_group(GroupName, _Config) when GroupName =:= admin_http; + GroupName =:= admin_cli; + GroupName =:= user; + GroupName =:= domain_admin_private -> + graphql_helper:clean(); end_per_group(_GroupName, _Config) -> - escalus_fresh:clean(), - graphql_helper:clean(). + escalus_fresh:clean(). init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). @@ -113,6 +154,23 @@ parse_xml_error(Config, Alice) -> ResultSet = user_set_private(Alice, <<"AAAABBBB">>, Config), ?assertEqual(<<"parse_error">>, get_err_code(ResultSet)). +% User private not configured test cases + +user_set_private_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_set_private_not_configured/2). + +user_set_private_not_configured(Config, Alice) -> + ElemStr = exml:to_binary(private_input()), + Res = user_set_private(Alice, ElemStr, Config), + get_not_loaded(Res). + +user_get_private_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_get_private_not_configured/2). + +user_get_private_not_configured(Config, Alice) -> + Res = user_get_private(Alice, <<"my_element">>, <<"alice:private:ns">>, Config), + get_not_loaded(Res). + % Admin tests admin_set_private(Config) -> @@ -155,7 +213,26 @@ private_input() -> attrs = [{<<"xmlns">>, "alice:private:ns"}], children = [{xmlcdata, <<"DATA">>}]}. -%% Domain admin tests +% Admin private not configured test cases + +admin_set_private_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_set_private_not_configured/2). + +admin_set_private_not_configured(Config, Alice) -> + AliceBin = user_to_bin(Alice), + ElemStr = exml:to_binary(private_input()), + Res = admin_set_private(AliceBin, ElemStr, Config), + get_not_loaded(Res). + +admin_get_private_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_get_private_not_configured/2). + +admin_get_private_not_configured(Config, Alice) -> + AliceBin = user_to_bin(Alice), + Res = admin_get_private(AliceBin, <<"my_element">>, <<"alice:private:ns">>, Config), + get_not_loaded(Res). + +% Domain admin tests domain_admin_user_set_private_no_permission(Config) -> escalus:fresh_story_with_config(Config, [{alice_bis, 1}], diff --git a/priv/graphql/schemas/admin/private.gql b/priv/graphql/schemas/admin/private.gql index 01d3d83f3ac..8413237d2d8 100644 --- a/priv/graphql/schemas/admin/private.gql +++ b/priv/graphql/schemas/admin/private.gql @@ -1,17 +1,17 @@ """ Allow admin to set the user's private data """ -type PrivateAdminMutation @protected { +type PrivateAdminMutation @protected @use(modules: ["mod_private"]){ "Set the user's private data" setPrivate(user: JID!, elementString: String!): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } """ Allow admin to get the user's private data """ -type PrivateAdminQuery @protected { +type PrivateAdminQuery @protected @use(modules: ["mod_private"]){ "Get the user's private data" getPrivate(user: JID!, element: String!, nameSpace: String!): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } diff --git a/priv/graphql/schemas/user/private.gql b/priv/graphql/schemas/user/private.gql index 358290e8df5..f8f2d591657 100644 --- a/priv/graphql/schemas/user/private.gql +++ b/priv/graphql/schemas/user/private.gql @@ -1,15 +1,15 @@ """ Allow user to set own private """ -type PrivateUserMutation @protected { +type PrivateUserMutation @protected @use(modules: ["mod_private"]){ "Set user's own private" - setPrivate(elementString: String!): String + setPrivate(elementString: String!): String @use } """ Allow user to get own private """ -type PrivateUserQuery @protected { +type PrivateUserQuery @protected @use(modules: ["mod_private"]){ "Get user's own private" - getPrivate(element: String!, nameSpace: String!): String + getPrivate(element: String!, nameSpace: String!): String @use } diff --git a/src/mod_private_api.erl b/src/mod_private_api.erl index ce6a1ca6f12..046eaddb296 100644 --- a/src/mod_private_api.erl +++ b/src/mod_private_api.erl @@ -19,7 +19,7 @@ private_get(JID, Element, Ns) -> end. -spec private_set(jid:jid(), ElementString :: binary()) -> {Res, iolist()} when - Res :: ok | not_found | not_loaded | parse_error. + Res :: ok | not_found | parse_error. private_set(JID, ElementString) -> case exml:parse(ElementString) of {error, Error} -> @@ -39,28 +39,16 @@ do_private_get(JID, Element, Ns) -> children = [SubEl] }] = ResIq#iq.sub_el, exml:to_binary(SubEl). -do_private_set(JID, Xml) -> +do_private_set(#jid{lserver = Domain} = JID, Xml) -> case ejabberd_auth:does_user_exist(JID) of true -> - do_private_set2(JID, Xml); - false -> - {not_found, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])} - end. - -do_private_set2(#jid{lserver = Domain} = JID, Xml) -> - {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), - case is_private_module_loaded(HostType) of - true -> + {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), send_iq(set, Xml, JID, HostType), {ok, ""}; false -> - {not_loaded, io_lib:format("Module mod_private is not loaded on domain ~s", [Domain])} + {not_found, io_lib:format("User ~s does not exist", [jid:to_binary(JID)])} end. --spec is_private_module_loaded(jid:server()) -> true | false. -is_private_module_loaded(Server) -> - lists:member(mod_private, gen_mod:loaded_modules(Server)). - send_iq(Method, Xml, From = To = _JID, HostType) -> IQ = {iq, <<"">>, Method, ?NS_PRIVATE, <<"">>, #xmlel{ name = <<"query">>, From 78f130ea5efc19c1fe2804650bbc8391cfa08e4d Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 28 Sep 2022 10:15:54 +0200 Subject: [PATCH 109/117] Adding @use directive and tests in mod_auth_token --- big_tests/tests/graphql_token_SUITE.erl | 94 +++++++++++++++++++++++-- priv/graphql/schemas/admin/token.gql | 6 +- priv/graphql/schemas/user/token.gql | 6 +- 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/big_tests/tests/graphql_token_SUITE.erl b/big_tests/tests/graphql_token_SUITE.erl index f3bc902565d..1924b2d92b7 100644 --- a/big_tests/tests/graphql_token_SUITE.erl +++ b/big_tests/tests/graphql_token_SUITE.erl @@ -4,7 +4,7 @@ -import(distributed_helper, [require_rpc_nodes/1, mim/0]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, - get_ok_value/2, get_err_code/1, get_unauthorized/1]). + get_ok_value/2, get_err_code/1, get_unauthorized/1, get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). @@ -18,16 +18,32 @@ all() -> {group, admin_cli}]. groups() -> - [{user, [], user_tests()}, + [{user, [], user_groups()}, {domain_admin, domain_admin_tests()}, - {admin_http, [], admin_tests()}, - {admin_cli, [], admin_tests()}]. + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {user_token_configured, [], user_tests()}, + {user_token_not_configured, [], user_token_not_configured_tests()}, + {admin_token_configured, [], admin_tests()}, + {admin_token_not_configured, [], admin_token_not_configured_tests()}]. + +user_groups() -> + [{group, user_token_configured}, + {group, user_token_not_configured}]. + +admin_groups() -> + [{group, admin_token_configured}, + {group, admin_token_not_configured}]. user_tests() -> [user_request_token_test, user_revoke_token_no_token_before_test, user_revoke_token_test]. +user_token_not_configured_tests() -> + [user_request_token_test_not_configured, + user_revoke_token_test_not_configured]. + domain_admin_tests() -> [admin_request_token_test, domain_admin_request_token_no_permission_test, @@ -42,6 +58,10 @@ admin_tests() -> admin_revoke_token_no_token_test, admin_revoke_token_test]. +admin_token_not_configured_tests() -> + [admin_request_token_test_not_configured, + admin_revoke_token_test_not_configured]. + init_per_suite(Config0) -> case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of true -> @@ -75,12 +95,33 @@ init_per_group(admin_http, Config) -> init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin, Config) -> - graphql_helper:init_domain_admin_handler(Config); + Config1 = ensure_token_started(Config), + graphql_helper:init_domain_admin_handler(Config1); +init_per_group(Group, Config) when Group =:= admin_token_configured; + Group =:= user_token_configured -> + ensure_token_started(Config); +init_per_group(Group, Config) when Group =:= admin_token_not_configured; + Group =:= user_token_not_configured -> + ensure_token_stopped(Config); init_per_group(user, Config) -> graphql_helper:init_user(Config). -end_per_group(_, _Config) -> - graphql_helper:clean(), +ensure_token_started(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:ensure_modules(HostType, required_modules()), + Config. + +ensure_token_stopped(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:ensure_modules(HostType, [{mod_auth_token, stopped}]), + Config. + +end_per_group(GroupName, _Config) when GroupName =:= admin_http; + GroupName =:= admin_cli; + GroupName =:= user; + GroupName =:= domain_admin -> + graphql_helper:clean(); +end_per_group(_GroupName, _Config) -> escalus_fresh:clean(). init_per_testcase(CaseName, Config) -> @@ -117,6 +158,24 @@ user_revoke_token_test(Config, Alice) -> ParsedRes = get_ok_value([data, token, revokeToken], Res2), ?assertEqual(<<"Revoked.">>, ParsedRes). +% User test cases mod_token not configured + +user_request_token_test_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_request_token_test_not_configured/2). + +user_request_token_test_not_configured(Config, Alice) -> + Res = user_request_token(Alice, Config), + get_not_loaded(Res). + +user_revoke_token_test_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_revoke_token_test_not_configured/2). + +user_revoke_token_test_not_configured(Config, Alice) -> + Res = user_revoke_token(Alice, Config), + get_not_loaded(Res). + % Domain admin tests domain_admin_request_token_no_permission_test(Config) -> @@ -134,6 +193,7 @@ domain_admin_request_token_no_permission_test(Config, AliceBis) -> domain_admin_revoke_token_no_permission_test(Config) -> escalus:fresh_story_with_config(Config, [{alice_bis, 1}], fun domain_admin_revoke_token_no_permission_test/2). + domain_admin_revoke_token_no_permission_test(Config, AliceBis) -> % External domain user Res = admin_revoke_token(user_to_bin(AliceBis), Config), @@ -178,6 +238,26 @@ admin_revoke_token_test(Config, Alice) -> ParsedRes = get_ok_value([data, token, revokeToken], Res2), ?assertEqual(<<"Revoked.">>, ParsedRes). +% Admin test cases token not configured + +admin_request_token_test_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_request_token_test_not_configured/2). + +admin_request_token_test_not_configured(Config, Alice) -> + Res = admin_request_token(user_to_bin(Alice), Config), + get_not_loaded(Res). + +admin_revoke_token_test_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_revoke_token_test_not_configured/2). + +admin_revoke_token_test_not_configured(Config, Alice) -> + Res = admin_request_token(user_to_bin(Alice), Config), + get_not_loaded(Res). + +% Commands + user_request_token(User, Config) -> execute_user_command(<<"token">>, <<"requestToken">>, User, #{}, Config). diff --git a/priv/graphql/schemas/admin/token.gql b/priv/graphql/schemas/admin/token.gql index 0f94b9a0cb5..8c38935fca9 100644 --- a/priv/graphql/schemas/admin/token.gql +++ b/priv/graphql/schemas/admin/token.gql @@ -1,11 +1,11 @@ """ Allow admin to get and revoke user's auth tokens """ - type TokenAdminMutation @protected { + type TokenAdminMutation @protected @use(modules: ["mod_auth_token"]){ "Request auth token for a user" requestToken(user: JID!): Token - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") "Revoke any tokens for a user" revokeToken(user: JID!): String - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } diff --git a/priv/graphql/schemas/user/token.gql b/priv/graphql/schemas/user/token.gql index f8beed436e5..6f7e7bab1c1 100644 --- a/priv/graphql/schemas/user/token.gql +++ b/priv/graphql/schemas/user/token.gql @@ -1,9 +1,9 @@ """ Allow user to get and revoke tokens. """ - type TokenUserMutation @protected{ + type TokenUserMutation @protected @use(modules: ["mod_auth_token"]){ "Get a new token" - requestToken: Token + requestToken: Token @use "Revoke any tokens" - revokeToken: String + revokeToken: String @use } From 953fe990fa7a7e899bbd904ddc80bdbc18097cf4 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Fri, 30 Sep 2022 12:39:46 +0200 Subject: [PATCH 110/117] Adding @use directive to vcard schemas and tests to graphlq_vcard --- big_tests/tests/graphql_vcard_SUITE.erl | 123 +++++++++++++++++++++--- priv/graphql/schemas/admin/vcard.gql | 8 +- priv/graphql/schemas/user/vcard.gql | 8 +- src/vcard/mod_vcard_api.erl | 8 +- 4 files changed, 123 insertions(+), 24 deletions(-) diff --git a/big_tests/tests/graphql_vcard_SUITE.erl b/big_tests/tests/graphql_vcard_SUITE.erl index 9e894006957..1781c02607f 100644 --- a/big_tests/tests/graphql_vcard_SUITE.erl +++ b/big_tests/tests/graphql_vcard_SUITE.erl @@ -5,7 +5,7 @@ -import(distributed_helper, [require_rpc_nodes/1, mim/0]). -import(graphql_helper, [execute_command/4, execute_user_command/5, user_to_bin/1, get_ok_value/2, skip_null_fields/1, get_err_msg/1, - get_unauthorized/1]). + get_unauthorized/1, get_not_loaded/1, get_err_code/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -14,16 +14,28 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_vcard}, + [{group, user}, {group, domain_admin_vcard}, - {group, admin_vcard_http}, - {group, admin_vcard_cli}]. + {group, admin_http}, + {group, admin_cli}]. groups() -> - [{user_vcard, [], user_vcard_tests()}, + [{user, [], user_groups()}, {domain_admin_vcard, [], domain_admin_vcard_tests()}, - {admin_vcard_http, [], admin_vcard_tests()}, - {admin_vcard_cli, [], admin_vcard_tests()}]. + {admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {user_vcard_configured, [], user_vcard_tests()}, + {user_vcard_not_configured, [], user_vcard_not_configured_tests()}, + {admin_vcard_configured, [], admin_vcard_tests()}, + {admin_vcard_not_configured, [], admin_vcard_not_configured_tests()}]. + +user_groups() -> + [{group, user_vcard_configured}, + {group, user_vcard_not_configured}]. + +admin_groups() -> + [{group, admin_vcard_configured}, + {group, admin_vcard_not_configured}]. user_vcard_tests() -> [user_set_vcard, @@ -33,6 +45,11 @@ user_vcard_tests() -> user_get_others_vcard_no_user, user_get_others_vcard_no_vcard]. +user_vcard_not_configured_tests() -> + [user_set_vcard_not_configured, + user_get_their_vcard_not_configured, + user_get_others_vcard_not_configured]. + domain_admin_vcard_tests() -> [admin_set_vcard, admin_set_vcard_incomplete_fields, @@ -51,33 +68,62 @@ admin_vcard_tests() -> admin_get_vcard_no_vcard, admin_get_vcard_no_user]. +admin_vcard_not_configured_tests() -> + [admin_set_vcard_not_configured, + admin_get_vcard_not_configured]. + init_per_suite(Config) -> case vcard_helper:is_vcard_ldap() of true -> {skip, ldap_vcard_is_not_supported}; _ -> + HostType = domain_helper:host_type(), Config1 = escalus:init_per_suite(Config), Config2 = ejabberd_node_utils:init(mim(), Config1), - dynamic_modules:save_modules(domain_helper:host_type(), Config2) + Config3 = dynamic_modules:save_modules(domain_helper:host_type(), Config2), + VCardOpts = dynamic_modules:get_saved_config(HostType, mod_vcard, Config3), + [{mod_vcard_opts, VCardOpts} | Config3] end. end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). -init_per_group(admin_vcard_http, Config) -> +init_per_group(admin_http, Config) -> graphql_helper:init_admin_handler(Config); -init_per_group(admin_vcard_cli, Config) -> +init_per_group(admin_cli, Config) -> graphql_helper:init_admin_cli(Config); init_per_group(domain_admin_vcard, Config) -> - graphql_helper:init_domain_admin_handler(Config); -init_per_group(user_vcard, Config) -> - graphql_helper:init_user(Config). - + Config1 = ensure_vcard_started(Config), + graphql_helper:init_domain_admin_handler(Config1); +init_per_group(user, Config) -> + graphql_helper:init_user(Config); +init_per_group(Group, Config) when Group =:= admin_vcard_configured; + Group =:= user_vcard_configured -> + ensure_vcard_started(Config); +init_per_group(Group, Config) when Group =:= admin_vcard_not_configured; + Group =:= user_vcard_not_configured -> + ensure_vcard_stopped(Config). + +end_per_group(GroupName, _Config) when GroupName =:= admin_http; + GroupName =:= admin_cli; + GroupName =:= user; + GroupName =:= domain_admin_vcard -> + graphql_helper:clean(); end_per_group(_GroupName, _Config) -> - graphql_helper:clean(), escalus_fresh:clean(). +ensure_vcard_started(Config) -> + HostType = domain_helper:host_type(), + VCardConfig = ?config(mod_vcard_opts, Config), + dynamic_modules:restart(HostType, mod_vcard, VCardConfig), + Config. + +ensure_vcard_stopped(Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:stop(HostType, mod_vcard), + Config. + init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). @@ -156,6 +202,33 @@ user_get_others_vcard_no_user(Config, Alice) -> Result = user_get_vcard(Alice, <<"AAAAA">>, Config), ?assertEqual(<<"User does not exist">>, get_err_msg(Result)). +% User VCard not configured test cases + +user_set_vcard_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_set_vcard_not_configured/2). + +user_set_vcard_not_configured(Config, Alice) -> + Vcard = complete_vcard_input(), + Res = user_set_vcard(Alice, Vcard, Config), + get_not_loaded(Res). + +user_get_others_vcard_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_get_others_vcard_not_configured/3). + +user_get_others_vcard_not_configured(Config, Alice, Bob) -> + Res = user_get_vcard(Alice, user_to_bin(Bob), Config), + get_not_loaded(Res). + +user_get_their_vcard_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_their_vcard_not_configured/2). + +user_get_their_vcard_not_configured(Config, Alice) -> + Res = user_get_own_vcard(Alice, Config), + ?assertEqual(<<"vcard_not_configured_error">>, get_err_code(Res)). + %% Domain admin test cases domain_admin_set_vcard_no_user(Config) -> @@ -239,6 +312,26 @@ admin_get_vcard_no_user(Config) -> Result = get_vcard(<<"AAAAA">>, Config), ?assertEqual(<<"User does not exist">>, get_err_msg(Result)). +%% Admin VCard not configured test cases + +admin_get_vcard_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_vcard_not_configured/2). + +admin_get_vcard_not_configured(Config, Alice) -> + Res = get_vcard(user_to_bin(Alice), Config), + get_not_loaded(Res). + +admin_set_vcard_not_configured(Config) -> + Config1 = [{vcard, complete_vcard_input()} | Config], + escalus:fresh_story_with_config(Config1, [{alice, 1}], + fun admin_set_vcard_not_configured/2). + +admin_set_vcard_not_configured(Config, Alice) -> + Vcard = ?config(vcard, Config), + Res = set_vcard(Vcard, user_to_bin(Alice), Config), + get_not_loaded(Res). + %% Commands user_set_vcard(User, VCard, Config) -> diff --git a/priv/graphql/schemas/admin/vcard.gql b/priv/graphql/schemas/admin/vcard.gql index b7eb69319fe..0234004f199 100644 --- a/priv/graphql/schemas/admin/vcard.gql +++ b/priv/graphql/schemas/admin/vcard.gql @@ -1,17 +1,17 @@ """ Allow admin to set user's vcard """ -type VcardAdminMutation @protected{ +type VcardAdminMutation @protected @use(modules: ["mod_vcard"]){ "Set a new vcard for a user" setVcard(user: JID!, vcard: VcardInput!): Vcard - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } """ Allow admin to get the user's vcard """ -type VcardAdminQuery @protected{ +type VcardAdminQuery @protected @use(modules: ["mod_vcard"]){ "Get the user's vcard" getVcard(user: JID!): Vcard - @protected(type: DOMAIN, args: ["user"]) + @protected(type: DOMAIN, args: ["user"]) @use(arg: "user") } diff --git a/priv/graphql/schemas/user/vcard.gql b/priv/graphql/schemas/user/vcard.gql index 166468ea528..305043342dd 100644 --- a/priv/graphql/schemas/user/vcard.gql +++ b/priv/graphql/schemas/user/vcard.gql @@ -1,15 +1,15 @@ """ Allow user to set own vcard """ -type VcardUserMutation @protected{ +type VcardUserMutation @protected @use(modules: ["mod_vcard"]){ "Set user's own vcard" - setVcard(vcard: VcardInput!): Vcard + setVcard(vcard: VcardInput!): Vcard @use } """ Allow user to get user's vcard """ -type VcardUserQuery @protected{ +type VcardUserQuery @protected @use(modules: ["mod_vcard"]){ "Get user's vcard" - getVcard(user: JID): Vcard + getVcard(user: JID): Vcard @use(arg: "user") } diff --git a/src/vcard/mod_vcard_api.erl b/src/vcard/mod_vcard_api.erl index 172d3ca7cb2..8f0401e8135 100644 --- a/src/vcard/mod_vcard_api.erl +++ b/src/vcard/mod_vcard_api.erl @@ -32,7 +32,13 @@ set_vcard(#jid{luser = LUser, lserver = LServer} = UserJID, Vcard) -> get_vcard(#jid{luser = LUser, lserver = LServer}) -> case mongoose_domain_api:get_domain_host_type(LServer) of {ok, HostType} -> - get_vcard_from_db(HostType, LUser, LServer); + % check if mod_vcard is loaded is needed in user's get_vcard command, when user variable is not passed + case gen_mod:is_loaded(HostType, mod_vcard) of + true -> + get_vcard_from_db(HostType, LUser, LServer); + false -> + {vcard_not_configured_error, "Mod_vcard is not loaded for this host"} + end; _ -> {not_found, "User does not exist"} end. From 1aa0d8d7de726ae82f26f45097c7ad9420dd95fa Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Fri, 30 Sep 2022 12:44:49 +0200 Subject: [PATCH 111/117] Adding comments to schemas --- priv/graphql/schemas/admin/muc.gql | 2 ++ priv/graphql/schemas/admin/muc_light.gql | 1 + priv/graphql/schemas/user/muc.gql | 2 ++ priv/graphql/schemas/user/muc_light.gql | 1 + priv/graphql/schemas/user/vcard.gql | 2 ++ 5 files changed, 8 insertions(+) diff --git a/priv/graphql/schemas/admin/muc.gql b/priv/graphql/schemas/admin/muc.gql index 9fc23919977..e74c57e856b 100644 --- a/priv/graphql/schemas/admin/muc.gql +++ b/priv/graphql/schemas/admin/muc.gql @@ -3,6 +3,7 @@ Allow admin to manage Multi-User Chat rooms. """ type MUCAdminMutation @protected @use(modules: ["mod_muc"]){ "Create a MUC room under the given XMPP hostname" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code createInstantRoom(mucDomain: String!, name: String!, owner: JID!, nick: String!): MUCRoomDesc @protected(type: DOMAIN, args: ["owner"]) "Invite a user to a MUC room" @@ -42,6 +43,7 @@ Allow admin to get information about Multi-User Chat rooms. """ type MUCAdminQuery @protected @use(modules: ["mod_muc"]){ "Get MUC rooms under the given MUC domain" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code listRooms(mucDomain: String!, from: JID, limit: Int, index: Int): MUCRoomsPayload! @protected(type: DOMAIN, args: ["from"]) "Get configuration of the MUC room" diff --git a/priv/graphql/schemas/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql index 4ba7ab9979a..ae148a6e630 100644 --- a/priv/graphql/schemas/admin/muc_light.gql +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -3,6 +3,7 @@ Allow admin to manage Multi-User Chat Light rooms. """ type MUCLightAdminMutation @use(modules: ["mod_muc_light"]) @protected{ "Create a MUC light room under the given XMPP hostname" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code createRoom(mucDomain: String!, name: String!, owner: JID!, subject: String!, id: NonEmptyString, options: [RoomConfigDictEntryInput!]): Room @protected(type: DOMAIN, args: ["owner"]) "Change configuration of a MUC Light room" diff --git a/priv/graphql/schemas/user/muc.gql b/priv/graphql/schemas/user/muc.gql index 5eee155f9e4..1430afbe9cf 100644 --- a/priv/graphql/schemas/user/muc.gql +++ b/priv/graphql/schemas/user/muc.gql @@ -3,6 +3,7 @@ Allow user to manage Multi-User Chat rooms. """ type MUCUserMutation @protected @use(modules: ["mod_muc"]){ "Create a MUC room under the given XMPP hostname" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code createInstantRoom(mucDomain: String!, name: String!, nick: String!): MUCRoomDesc "Invite a user to a MUC room" inviteUser(room: JID!, recipient: JID!, reason: String): String @use(arg: "room") @@ -31,6 +32,7 @@ Allow user to get information about Multi-User Chat rooms. """ type MUCUserQuery @protected @use(modules: ["mod_muc"]){ "Get MUC rooms under the given MUC domain" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code listRooms(mucDomain: String!, limit: Int, index: Int): MUCRoomsPayload! "Get configuration of the MUC room" getRoomConfig(room: JID!): MUCRoomConfig @use(arg: "room") diff --git a/priv/graphql/schemas/user/muc_light.gql b/priv/graphql/schemas/user/muc_light.gql index 108363a18c1..3263fa692fb 100644 --- a/priv/graphql/schemas/user/muc_light.gql +++ b/priv/graphql/schemas/user/muc_light.gql @@ -3,6 +3,7 @@ Allow user to manage Multi-User Chat Light rooms. """ type MUCLightUserMutation @protected @use(modules: ["mod_muc_light"]){ "Create a MUC light room under the given XMPP hostname" + #There is no @use directive because it is currently impossible to get HostType from mucDomain in directive code createRoom(mucDomain: String!, name: String!, subject: String!, id: NonEmptyString, options: [RoomConfigDictEntryInput!]): Room "Change configuration of a MUC Light room" changeRoomConfiguration(room: JID!, name: String!, subject: String!, options: [RoomConfigDictEntryInput!]): Room @use(arg: "room") diff --git a/priv/graphql/schemas/user/vcard.gql b/priv/graphql/schemas/user/vcard.gql index 305043342dd..c9fe960c26d 100644 --- a/priv/graphql/schemas/user/vcard.gql +++ b/priv/graphql/schemas/user/vcard.gql @@ -11,5 +11,7 @@ Allow user to get user's vcard """ type VcardUserQuery @protected @use(modules: ["mod_vcard"]){ "Get user's vcard" + #In mod_vcard_api was left the check if the mod_vcard is loaded, because, + #when get_vcard is called without user variable @use directive cannot check if the module is loaded. getVcard(user: JID): Vcard @use(arg: "user") } From 655ac91d764cf9bce44b525fd6c5782774e18d9b Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Fri, 30 Sep 2022 15:53:06 +0200 Subject: [PATCH 112/117] Fixing mod_last tests --- big_tests/tests/graphql_last_SUITE.erl | 34 +++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 81e31a0e3ec..a2f2b8e8745 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -36,6 +36,8 @@ groups() -> {admin_old_users, [], admin_old_users_tests()}, {domain_admin_old_users, [], domain_admin_old_users_tests()}, {admin_last_not_configured, [], admin_last_not_configured()}, + {admin_last_not_configured_old_users, [], admin_last_not_configured_old_users()}, + {admin_last_not_configured_group, [], admin_last_not_configured_groups()}, {admin_last_configured, [], admin_last_configured()}, {user_last_not_configured, [], user_last_not_configured()}]. @@ -45,7 +47,7 @@ user_groups() -> admin_groups() -> [{group, admin_last_configured}, - {group, admin_last_not_configured}]. + {group, admin_last_not_configured_group}]. admin_last_configured() -> [{group, admin_last}, @@ -55,6 +57,10 @@ domain_admin_groups() -> [{group, domain_admin_last}, {group, domain_admin_old_users}]. +admin_last_not_configured_groups() -> + [{group, admin_last_not_configured}, + {group, admin_last_not_configured_old_users}]. + user_tests() -> [user_set_last, user_get_last, @@ -105,11 +111,13 @@ domain_admin_old_users_tests() -> admin_last_not_configured() -> [admin_set_last_not_configured, admin_get_last_not_configured, - admin_count_active_users_last_not_configured, - admin_remove_old_users_domain_last_not_configured, - admin_remove_old_users_global_last_not_configured, - admin_list_old_users_domain_last_not_configured, - admin_list_old_users_global_last_not_configured]. + admin_count_active_users_last_not_configured]. + +admin_last_not_configured_old_users() -> + [admin_remove_old_users_domain_last_not_configured, + admin_remove_old_users_global_last_not_configured, + admin_list_old_users_domain_last_not_configured, + admin_list_old_users_global_last_not_configured]. init_per_suite(Config) -> HostType = domain_helper:host_type(), @@ -142,20 +150,18 @@ init_per_group(admin_last, Config) -> Config; init_per_group(admin_last_configured, Config) -> configure_last(Config); -init_per_group(admin_last_not_configured, Config) -> +init_per_group(admin_last_not_configured_group, Config) -> stop_last(Config); -init_per_group(admin_old_users, Config) -> +init_per_group(Group, Config) when Group =:= admin_old_users; + Group =:= domain_admin_old_users; + Group =:= admin_last_not_configured_old_users -> AuthMods = mongoose_helper:auth_modules(), case lists:member(ejabberd_auth_ldap, AuthMods) of true -> {skip, not_fully_supported_with_ldap}; false -> Config end; -init_per_group(domain_admin_old_users, Config) -> - AuthMods = mongoose_helper:auth_modules(), - case lists:member(ejabberd_auth_ldap, AuthMods) of - true -> {skip, not_fully_supported_with_ldap}; - false -> Config - end. +init_per_group(admin_last_not_configured, Config) -> + Config. configure_last(Config) -> HostType = domain_helper:host_type(), From 50a99aabbf81a0f4c0bc45f2566142d840cb3d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 5 Oct 2022 16:48:17 +0200 Subject: [PATCH 113/117] Make it possible to use more defaults in mongooseim.toml They are documented, and the user could define them with different values if necessary. --- rel/files/mongooseim.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rel/files/mongooseim.toml b/rel/files/mongooseim.toml index b9f12446eeb..af180bde90f 100644 --- a/rel/files/mongooseim.toml +++ b/rel/files/mongooseim.toml @@ -5,8 +5,12 @@ default_server_domain = {{{default_server_domain}}} registration_timeout = "infinity" language = "en" + {{#all_metrics_are_global}} all_metrics_are_global = {{{all_metrics_are_global}}} + {{/all_metrics_are_global}} + {{#sm_backend}} sm_backend = {{{sm_backend}}} + {{/sm_backend}} max_fsm_queue = 1000 {{#http_server_name}} http_server_name = {{{http_server_name}}} @@ -351,7 +355,9 @@ {{#s2s_certfile}} certfile = {{{s2s_certfile}}} {{/s2s_certfile}} + {{#s2s_default_policy}} default_policy = {{{s2s_default_policy}}} + {{/s2s_default_policy}} outgoing.port = {{{outgoing_s2s_port}}} {{#s2s_addr}} From d609dde3d3e67082ab08f4960f8ae5a344de013c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 5 Oct 2022 16:50:11 +0200 Subject: [PATCH 114/117] Reorganize overlay vars to avoid overriding There is an issue with the overriding logic in relx: included vars override previously defined ones, but there is no simple way of overriding the included vars other than including another file. This way making the dev releases resulted in incorrect vars. Big tests succeeded because they had their own overriding logic. It seems that we could do without overriding entirely, and this change shows that the resulting files are actually more straightforward. --- rel/fed1.vars-toml.config | 31 +++++++++---------------------- rel/mim1.vars-toml.config | 27 ++++++++++++++++++--------- rel/mim2.vars-toml.config | 27 ++++++--------------------- rel/mim3.vars-toml.config | 32 ++++++++++---------------------- rel/prod.vars-toml.config | 38 ++++++++++++++++++++++++++++++++++++++ rel/reg1.vars-toml.config | 32 ++++++++++---------------------- rel/vars-toml.config | 39 ++------------------------------------- 7 files changed, 93 insertions(+), 133 deletions(-) create mode 100644 rel/prod.vars-toml.config diff --git a/rel/fed1.vars-toml.config b/rel/fed1.vars-toml.config index 25b9d6cb45f..b6e34e8d401 100644 --- a/rel/fed1.vars-toml.config +++ b/rel/fed1.vars-toml.config @@ -1,16 +1,17 @@ -"./vars-toml.config". - +%% vm.args {node_name, "fed1@localhost"}. +%% mongooseim.toml {c2s_port, 5242}. +{outgoing_s2s_port, 5269}. {incoming_s2s_port, 5299}. {http_port, 5282}. {https_port, 5287}. -{http_api_endpoint_port, 5294}. -{http_api_client_endpoint_port, 8095}. {http_graphql_api_admin_endpoint_port, 5556}. {http_graphql_api_domain_admin_endpoint_port, 5546}. {http_graphql_api_user_endpoint_port, 5566}. +{http_api_endpoint_port, 5294}. +{http_api_client_endpoint_port, 8095}. %% This node is for s2s testing. %% "localhost" host should NOT be defined. @@ -38,30 +39,16 @@ host = \"domain.example.com\" ip_address = \"127.0.0.1\" "}. -{s2s_default_policy, "\"allow\""}. -{highload_vm_args, ""}. -{listen_service, false}. {tls_config, "tls.verify_mode = \"none\" tls.certfile = \"priv/ssl/fake_server.pem\" tls.mode = \"starttls\" tls.ciphers = \"ECDHE-RSA-AES256-GCM-SHA384\""}. -{http_api_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_endpoint_port }}"}. -{http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. -{http_graphql_api_admin_endpoint, "ip_address = \"127.0.0.1\" - port = {{http_graphql_api_admin_endpoint_port}}"}. -{http_graphql_api_domain_admin_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_domain_admin_endpoint_port}}"}. -{http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_user_endpoint_port}}"}. - {c2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. {s2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. -{mod_last, false}. -{mod_private, false}. -{mod_privacy, false}. -{mod_blocking, false}. -{mod_offline, false}. +{mod_cache_users, ""}. + +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/mim1.vars-toml.config b/rel/mim1.vars-toml.config index f85a780b024..73e7cd99c30 100644 --- a/rel/mim1.vars-toml.config +++ b/rel/mim1.vars-toml.config @@ -1,10 +1,21 @@ -"./vars-toml.config". +%% vm.args +{node_name, "mongooseim@localhost"}. +%% mongooseim.toml +{c2s_port, 5222}. {c2s_tls_port, 5223}. {outgoing_s2s_port, 5299}. +{incoming_s2s_port, 5269}. +{http_port, 5280}. +{https_port, 5285}. {service_port, 8888}. {kicking_service_port, 8666}. {hidden_service_port, 8189}. +{http_graphql_api_admin_endpoint_port, 5551}. +{http_graphql_api_domain_admin_endpoint_port, 5541}. +{http_graphql_api_user_endpoint_port, 5561}. +{http_api_endpoint_port, 8088}. +{http_api_client_endpoint_port, 8089}. {hosts, "\"localhost\", \"anonymous.localhost\", \"localhost.bis\""}. {host_types, "\"test type\", \"dummy auth\", \"anonymous\""}. @@ -41,10 +52,9 @@ {s2s_addr, "[[s2s.address]] host = \"fed1\" ip_address = \"127.0.0.1\""}. -{s2s_default_policy, "\"allow\""}. -% Disable highload args to save memory for dev builds -{highload_vm_args, ""}. +{tls_config, "tls.verify_mode = \"none\" + tls.certfile = \"priv/ssl/fake_server.pem\""}. {secondary_c2s, "[[listen.c2s]] @@ -79,11 +89,10 @@ {mod_cache_users, " time_to_live = 2 number_of_segments = 5\n"}. -{mod_last, false}. -{mod_private, false}. -{mod_privacy, false}. -{mod_blocking, false}. -{mod_offline, false}. {zlib, "10_000"}. + {c2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. {s2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. + +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/mim2.vars-toml.config b/rel/mim2.vars-toml.config index c155e82df7b..d02ab74982e 100644 --- a/rel/mim2.vars-toml.config +++ b/rel/mim2.vars-toml.config @@ -1,9 +1,10 @@ -"./vars-toml.config". - -{node_name, "ejabberd2@localhost"}. +%% vm.args +{node_name, "mongooseim2@localhost"}. +%% mongooseim.toml {c2s_port, 5232}. {c2s_tls_port, 5233}. +{outgoing_s2s_port, 5269}. {incoming_s2s_port, 5279}. {http_port, 5281}. {https_port, 5286}. @@ -20,18 +21,6 @@ {s2s_addr, "[[s2s.address]] host = \"localhost2\" ip_address = \"127.0.0.1\""}. -{s2s_default_policy, "\"allow\""}. -{highload_vm_args, ""}. - -{http_graphql_api_admin_endpoint, "ip_address = \"127.0.0.1\" - port = {{http_graphql_api_admin_endpoint_port}}"}. -{http_graphql_api_domain_admin_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_domain_admin_endpoint_port}}"}. -{http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_endpoint_port }}"}. -{http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. {tls_config, "tls.verify_mode = \"none\" tls.certfile = \"priv/ssl/fake_server.pem\" @@ -68,9 +57,5 @@ modules = { } auth.dummy = { }"}. -{mod_cache_users, false}. -{mod_last, false}. -{mod_private, false}. -{mod_privacy, false}. -{mod_blocking, false}. -{mod_offline, false}. +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/mim3.vars-toml.config b/rel/mim3.vars-toml.config index 4b33dbca101..eab4354adc5 100644 --- a/rel/mim3.vars-toml.config +++ b/rel/mim3.vars-toml.config @@ -1,18 +1,22 @@ -"./vars-toml.config". - +%% vm.args {node_name, "mongooseim3@localhost"}. +%% mongooseim.toml {c2s_port, 5262}. {c2s_tls_port, 5263}. {outgoing_s2s_port, 5295}. {incoming_s2s_port, 5291}. {http_port, 5283}. {https_port, 5290}. -{http_api_endpoint_port, 8092}. -{http_api_client_endpoint_port, 8193}. {http_graphql_api_admin_endpoint_port, 5553}. {http_graphql_api_domain_admin_endpoint_port, 5543}. {http_graphql_api_user_endpoint_port, 5563}. +{http_api_endpoint_port, 8092}. +{http_api_client_endpoint_port, 8193}. + +"./vars-toml.config". + +{node_name, "mongooseim3@localhost"}. {hosts, "\"localhost\", \"anonymous.localhost\", \"localhost.bis\""}. {default_server_domain, "\"localhost\""}. @@ -20,8 +24,6 @@ {s2s_addr, "[[s2s.address]] host = \"localhost2\" ip_address = \"127.0.0.1\""}. -{s2s_default_policy, "\"allow\""}. -{highload_vm_args, ""}. {listen_service, ""}. {tls_config, "tls.verify_mode = \"none\" @@ -41,22 +43,8 @@ tls.module = \"just_tls\" tls.ciphers = \"ECDHE-RSA-AES256-GCM-SHA384\""}. -{http_graphql_api_admin_endpoint, "ip_address = \"127.0.0.1\" - port = {{http_graphql_api_admin_endpoint_port}}"}. -{http_graphql_api_domain_admin_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_domain_admin_endpoint_port}}"}. -{http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_endpoint_port }}"}. -{http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. - {c2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. {s2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. -{mod_cache_users, false}. -{mod_last, false}. -{mod_private, false}. -{mod_privacy, false}. -{mod_blocking, false}. -{mod_offline, false}. +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/prod.vars-toml.config b/rel/prod.vars-toml.config new file mode 100644 index 00000000000..666ef865a3a --- /dev/null +++ b/rel/prod.vars-toml.config @@ -0,0 +1,38 @@ +%% vm.args +{node_name, "mongooseim@localhost"}. +{highload_vm_args, "+P 10000000 -env ERL_MAX_PORTS 250000"}. + +%% mongooseim.toml +{c2s_port, 5222}. +{outgoing_s2s_port, 5269}. +{incoming_s2s_port, 5269}. +{http_port, 5280}. +{https_port, 5285}. +{http_graphql_api_admin_endpoint_port, 5551}. +{http_graphql_api_domain_admin_endpoint_port, 5541}. +{http_graphql_api_user_endpoint_port, 5561}. +{http_api_endpoint_port, 8088}. +{http_api_client_endpoint_port, 8089}. + +{hosts, "\"localhost\""}. +{default_server_domain, "\"localhost\""}. +{s2s_default_policy, "\"deny\""}. +{listen_service, "[[listen.service]] + port = 8888 + access = \"all\" + shaper_rule = \"fast\" + ip_address = \"127.0.0.1\" + password = \"secret\""}. + +%% "" means that the module is enabled without any options +{mod_cache_users, ""}. +{mod_last, ""}. +{mod_offline, ""}. +{mod_privacy, ""}. +{mod_blocking, ""}. +{mod_private, ""}. +{tls_config, "tls.verify_mode = \"none\" + tls.certfile = \"priv/ssl/fake_server.pem\""}. + +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/reg1.vars-toml.config b/rel/reg1.vars-toml.config index ee541b2a75c..4b5a4e4fea7 100644 --- a/rel/reg1.vars-toml.config +++ b/rel/reg1.vars-toml.config @@ -1,17 +1,18 @@ -"./vars-toml.config". - +%% vm.args {node_name, "reg1@localhost"}. +%% mongooseim.toml {c2s_port, 5252}. +{outgoing_s2s_port, 5269}. {incoming_s2s_port, 5298}. {http_port, 5272}. {https_port, 5277}. {service_port, 9990}. -{http_api_endpoint_port, 8074}. -{http_api_client_endpoint_port, 8075}. -{http_qraphql_api_admin_endpoint_port, 5554}. +{http_graphql_api_admin_endpoint_port, 5554}. {http_graphql_api_domain_admin_endpoint_port, 5544}. {http_graphql_api_user_endpoint_port, 5564}. +{http_api_endpoint_port, 8074}. +{http_api_client_endpoint_port, 8075}. %% This node is for global distribution testing. %% reg is short for region. @@ -27,7 +28,6 @@ [[s2s.address]] host = \"localhost.bis\" ip_address = \"127.0.0.1\""}. -{s2s_default_policy, "\"allow\""}. {listen_service, "[[listen.service]] port = {{ service_port }} access = \"all\" @@ -39,23 +39,11 @@ tls.certfile = \"priv/ssl/fake_server.pem\" tls.mode = \"starttls\" tls.ciphers = \"ECDHE-RSA-AES256-GCM-SHA384\""}. -{secondary_c2s, ""}. - -{http_graphql_api_admin_endpoint, "ip_address = \"127.0.0.1\" - port = {{http_qraphql_api_admin_endpoint_port}}"}. -{http_graphql_api_domain_admin_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_domain_admin_endpoint_port}}"}. -{http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" - port = {{http_graphql_api_user_endpoint_port}}"}. -{http_api_endpoint, "ip_address = \"127.0.0.1\" - port = {{ http_api_endpoint_port }}"}. -{http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. {c2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. {s2s_dhfile, "\"priv/ssl/fake_dh_server.pem\""}. -{mod_last, false}. -{mod_private, false}. -{mod_privacy, false}. -{mod_blocking, false}. -{mod_offline, false}. +{mod_cache_users, ""}. + +%% Include common vars shared by all profiles +"./vars-toml.config". diff --git a/rel/vars-toml.config b/rel/vars-toml.config index 22a74cc83f4..d17f75d1fcb 100644 --- a/rel/vars-toml.config +++ b/rel/vars-toml.config @@ -1,41 +1,7 @@ -{node_name, "mongooseim@localhost"}. - -{c2s_port, 5222}. -{outgoing_s2s_port, 5269}. -{incoming_s2s_port, 5269}. -{http_port, 5280}. -{https_port, 5285}. -{http_graphql_api_admin_endpoint_port, 5551}. -{http_graphql_api_domain_admin_endpoint_port, 5541}. -{http_graphql_api_user_endpoint_port, 5561}. - -% vm.args -{highload_vm_args, "+P 10000000 -env ERL_MAX_PORTS 250000"}. - -% TOML config -{hosts, "\"localhost\""}. -{default_server_domain, "\"localhost\""}. -{s2s_default_policy, "\"deny\""}. -{listen_service, "[[listen.service]] - port = 8888 - access = \"all\" - shaper_rule = \"fast\" - ip_address = \"127.0.0.1\" - password = \"secret\""}. - %% "" means that the module is enabled without any options -{mod_cache_users, ""}. -{mod_last, ""}. -{mod_offline, ""}. -{mod_privacy, ""}. -{mod_blocking, ""}. -{mod_private, ""}. {mod_roster, ""}. {mod_vcard, " host = \"vjud.@HOST@\"\n"}. -{sm_backend, "\"mnesia\""}. {auth_method, "internal"}. -{tls_config, "tls.verify_mode = \"none\" - tls.certfile = \"priv/ssl/fake_server.pem\""}. {https_config, "tls.verify_mode = \"none\" tls.certfile = \"priv/ssl/fake_cert.pem\" tls.keyfile = \"priv/ssl/fake_key.pem\" @@ -47,11 +13,10 @@ {http_graphql_api_user_endpoint, "ip_address = \"0.0.0.0\" port = {{http_graphql_api_user_endpoint_port}}"}. {http_api_endpoint, "ip_address = \"127.0.0.1\" - port = 8088"}. -{http_api_client_endpoint, "port = 8089"}. + port = {{http_api_endpoint_port}}"}. +{http_api_client_endpoint, "port = {{ http_api_client_endpoint_port }}"}. {s2s_use_starttls, "\"optional\""}. {s2s_certfile, "\"priv/ssl/fake_server.pem\""}. -{all_metrics_are_global, "false"}. "./configure.vars.config". From 64ade6c1e0a9d4ff41fa5bfc9a93e49166224208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 5 Oct 2022 16:50:36 +0200 Subject: [PATCH 115/117] Reference the new vars for prod --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index f1300c1ad57..6f34b6eec72 100644 --- a/rebar.config +++ b/rebar.config @@ -160,7 +160,7 @@ ]}. {profiles, [ {prod, [{relx, [ {dev_mode, false}, - {overlay_vars, "rel/vars-toml.config"}, + {overlay_vars, "rel/prod.vars-toml.config"}, {overlay, [{template, "rel/files/mongooseim.toml", "etc/mongooseim.toml"}]} ]}, {erl_opts, [{d, 'PROD_NODE'}]} ]}, %% development nodes From 9530b2a913595691d98c568b44485c26c0b03f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chrz=C4=85szcz?= Date: Wed, 5 Oct 2022 16:51:04 +0200 Subject: [PATCH 116/117] Update node name in big tests --- big_tests/dynamic_domains.config | 2 +- big_tests/test.config | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/big_tests/dynamic_domains.config b/big_tests/dynamic_domains.config index 0204b4ebf50..695970a5d64 100644 --- a/big_tests/dynamic_domains.config +++ b/big_tests/dynamic_domains.config @@ -27,7 +27,7 @@ {hidden_service_port, 8189}, {gd_endpoint_port, 5555}, {http_notifications_port, 8000}]}, - {mim2, [{node, ejabberd2@localhost}, + {mim2, [{node, mongooseim2@localhost}, {domain, <<"domain.example.com">>}, {host_type, <<"test type">>}, {dynamic_domains, [{<<"test type">>, [<<"domain.example.com">>]}]}, diff --git a/big_tests/test.config b/big_tests/test.config index 8a62490d0e4..ff86cdea6cc 100644 --- a/big_tests/test.config +++ b/big_tests/test.config @@ -7,7 +7,7 @@ %% See s2s_SUITE for example on using `hosts` to RPC into nodes (uses CT "require"). %% the Erlang node name of tested ejabberd/MongooseIM {ejabberd_node, 'mongooseim@localhost'}. -{ejabberd2_node, 'ejabberd2@localhost'}. +{ejabberd2_node, 'mongooseim2@localhost'}. {ejabberd_cookie, ejabberd}. {ejabberd_string_format, bin}. @@ -40,7 +40,7 @@ {hidden_service_port, 8189}, {gd_endpoint_port, 5555}, {http_notifications_port, 8000}]}, - {mim2, [{node, ejabberd2@localhost}, + {mim2, [{node, mongooseim2@localhost}, {domain, <<"localhost">>}, {host_type, <<"localhost">>}, {vars, "mim2"}, From 180c86dc1c4cf36651a0cc6eb1af0a1c27b5a779 Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 5 Oct 2022 08:56:17 +0200 Subject: [PATCH 117/117] Deleting unnecessary code from init_per_suite --- big_tests/tests/graphql_last_SUITE.erl | 7 +++---- big_tests/tests/graphql_muc_SUITE.erl | 8 ++++---- big_tests/tests/graphql_token_SUITE.erl | 1 - priv/graphql/schemas/admin/offline.gql | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index a2f2b8e8745..f208fd243af 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -122,10 +122,9 @@ admin_last_not_configured_old_users() -> init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), - Config1 = escalus:init_per_suite(Config), - Config2 = dynamic_modules:save_modules([HostType, SecHostType], Config1), - Config3 = ejabberd_node_utils:init(mim(), Config2), - escalus:init_per_suite(Config3). + Config1 = dynamic_modules:save_modules([HostType, SecHostType], Config), + Config2 = ejabberd_node_utils:init(mim(), Config1), + escalus:init_per_suite(Config2). end_per_suite(Config) -> dynamic_modules:restore_modules(Config), diff --git a/big_tests/tests/graphql_muc_SUITE.erl b/big_tests/tests/graphql_muc_SUITE.erl index b5b88c97c9b..02500978d14 100644 --- a/big_tests/tests/graphql_muc_SUITE.erl +++ b/big_tests/tests/graphql_muc_SUITE.erl @@ -92,7 +92,7 @@ user_muc_not_configured_tests() -> user_moderator_set_user_role_muc_not_configured, user_can_enter_room_muc_not_configured, user_can_exit_room_muc_not_configured, - user_list_room_affiliation_muc_not_configured]. + user_list_room_affiliations_muc_not_configured]. admin_muc_tests() -> [admin_create_and_delete_room, @@ -1610,11 +1610,11 @@ user_can_exit_room_muc_not_configured_story(Config, Alice) -> Res = user_exit_room(Alice, get_room_name(), <<"ali">>, Resource, Config), get_not_loaded(Res). -user_list_room_affiliation_muc_not_configured(Config) -> +user_list_room_affiliations_muc_not_configured(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], - fun user_list_room_affiliation_muc_not_configured_story/2). + fun user_list_room_affiliations_muc_not_configured_story/2). -user_list_room_affiliation_muc_not_configured_story(Config, Alice) -> +user_list_room_affiliations_muc_not_configured_story(Config, Alice) -> Res = user_list_room_affiliations(Alice, get_room_name(), owner, Config), get_not_loaded(Res). diff --git a/big_tests/tests/graphql_token_SUITE.erl b/big_tests/tests/graphql_token_SUITE.erl index 1924b2d92b7..aa49c37bb03 100644 --- a/big_tests/tests/graphql_token_SUITE.erl +++ b/big_tests/tests/graphql_token_SUITE.erl @@ -67,7 +67,6 @@ init_per_suite(Config0) -> true -> HostType = domain_helper:host_type(), Config = dynamic_modules:save_modules(HostType, Config0), - dynamic_modules:ensure_modules(HostType, required_modules()), Config1 = escalus:init_per_suite(Config), ejabberd_node_utils:init(mim(), Config1); false -> diff --git a/priv/graphql/schemas/admin/offline.gql b/priv/graphql/schemas/admin/offline.gql index 82eeae7500c..ca212fd5f28 100644 --- a/priv/graphql/schemas/admin/offline.gql +++ b/priv/graphql/schemas/admin/offline.gql @@ -1,7 +1,7 @@ """ Allow admin to delete offline messages from specified domain """ -type OjflineAdminMutation @protected @use(modules: ["mod_offline"]){ +type OfflineAdminMutation @protected @use(modules: ["mod_offline"]){ "Delete offline messages whose date has expired" deleteExpiredMessages(domain: String!): String @use(arg: "domain") @protected(type: DOMAIN, args: ["domain"])