diff --git a/Makefile b/Makefile index 38608568f3..029fe7bea8 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/big_tests/default.spec b/big_tests/default.spec index 6845a0b840..34b16dfb11 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -46,6 +46,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}. @@ -103,7 +104,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.config b/big_tests/dynamic_domains.config index 0204b4ebf5..695970a5d6 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/dynamic_domains.spec b/big_tests/dynamic_domains.spec index 26a47c0cfd..94c6c9912f 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -63,6 +63,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}. @@ -150,7 +151,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/run_common_test.erl b/big_tests/run_common_test.erl index 97bc316eb1..b3b6dc6d91 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/test.config b/big_tests/test.config index cf890598aa..ff86cdea6c 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}. @@ -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}, @@ -41,13 +40,12 @@ {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"}, {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/cluster_commands_SUITE.erl b/big_tests/tests/cluster_commands_SUITE.erl index ac058dd1f7..1e1e3bcc1c 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, []), + mongoose_server_api, leave_cluster, []), ct:pal("leave_using_rpc result ~p~n", [Result]), %% then distributed_helper:verify_result(Node2, remove), @@ -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 a71f4b4181..73783392fc 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/domain_helper.erl b/big_tests/tests/domain_helper.erl index 6cbdfa68f0..ffae07f3dc 100644 --- a/big_tests/tests/domain_helper.erl +++ b/big_tests/tests/domain_helper.erl @@ -1,22 +1,6 @@ -module(domain_helper). --export([insert_configured_domains/0, - delete_configured_domains/0, - insert_domain/3, - delete_domain/2, - 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/domain_removal_SUITE.erl b/big_tests/tests/domain_removal_SUITE.erl index dc1a6163f1..27c970949c 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}, @@ -25,7 +26,8 @@ all() -> {group, offline_removal}, {group, markers_removal}, {group, vcard_removal}, - {group, last_removal} + {group, last_removal}, + {group, removal_failures} ]. groups() -> @@ -34,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]}, @@ -43,7 +47,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 +85,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) -> @@ -89,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) -> @@ -166,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) @@ -178,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, @@ -394,25 +407,63 @@ 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) 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), diff --git a/big_tests/tests/domain_rest_helper.erl b/big_tests/tests/domain_rest_helper.erl index c85268ec11..b41d77e8c1 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/ejabberd_node_utils.erl b/big_tests/tests/ejabberd_node_utils.erl index eb2b5a1fcb..b3aa0432ef 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/big_tests/tests/graphql_account_SUITE.erl b/big_tests/tests/graphql_account_SUITE.erl index 4fb1e907e7..f5f56e323b 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, @@ -29,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, @@ -41,6 +49,29 @@ admin_account_tests() -> admin_ban_user, admin_change_user_password]. +domain_admin_tests() -> + [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_register_random_user_no_permission, + admin_remove_existing_user, + domain_admin_remove_user_no_permission, + 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], Config2 = escalus:init_per_suite(Config1), @@ -55,12 +86,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 +104,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 +130,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_no_permission = 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 +155,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_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) -> + {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). @@ -125,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 = <>, @@ -136,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), @@ -154,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), @@ -165,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">>)). @@ -190,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">>, @@ -236,30 +308,130 @@ 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). + +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 + 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_no_permission(Config) -> + % An unknown domain + 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_no_permission(Config) -> + Password = lists:last(escalus_users:get_usp(Config, alice)), + 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). + +domain_admin_check_password_hash_no_permission(Config) -> + ExternalUserSCRAM = escalus_users:get_jid(Config, alice_bis), + EmptyHash = list_to_binary(get_md5(<<>>)), + Method = <<"md5">>, + % An external domain user + 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_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_no_permission(Config) -> + ExternalBinJID = escalus_users:get_jid(Config, alice_bis), + % An external domain user + 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) -> + Password = <<"my_password">>, + Domain = <<"unknown-domain">>, + get_unauthorized(register_user(Domain, external_user, Password, 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_no_permission(Config) -> + Reason = <<"annoying">>, + % Ban not existing user + Resp1 = ban_user(?NOT_EXISTING_JID, Reason, Config), + get_unauthorized(Resp1), + % 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), + get_unauthorized(Resp2) + end). + +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), + % 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 c6ef6420d0..8fa4a3fd0b 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,18 +95,18 @@ 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, <<"hostType">> => ?HOST_TYPE, - <<"enabled">> => null}, ParsedResult). + <<"status">> => 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">>]), - ?assertEqual(#{host_type => ?HOST_TYPE, enabled => false}, Domain). + ?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(<<"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, <<"status">> := <<"ENABLED">>}, 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). + <<"status">> => <<"ENABLED">>}, 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,50 @@ 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, + <<"status">> => <<"ENABLED">>}, 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)), + 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(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, Config)). + +domain_admin_enable_domain_no_permission(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(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(?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(?DOMAIN_ADMIN_EXAMPLE_DOMAIN, ?HOST_TYPE, Config)). + +domain_admin_delete_domain_password_no_permission(Config) -> + get_unauthorized(delete_domain_password(?EXAMPLE_DOMAIN, Config)), + get_unauthorized(delete_domain_password(?DOMAIN_ADMIN_EXAMPLE_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 ce06709329..c862795692 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_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_gdpr_tests() -> + [admin_retrieve_user_data, + admin_gdpr_no_user_test, + domain_admin_retrieve_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(), @@ -50,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), @@ -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_retrieve_user_data_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice_bis, 1}], + fun domain_admin_retrieve_user_data_no_permission/2). + +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), + 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 99b651ec73..09d00ea3d9 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -11,8 +11,14 @@ -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) -> Request = - #{port => get_listener_port(EpName), + #{port => get_listener_port(Node, EpName), role => {graphql, EpName}, method => <<"POST">>, return_maps => true, @@ -26,21 +32,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 +67,38 @@ 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), + #{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), 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. @@ -100,13 +121,15 @@ 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)), 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. @@ -129,9 +152,14 @@ 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_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)). @@ -139,9 +167,11 @@ 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_bad_request({Code, _Msg}) -> + assert_response_code(bad_request, 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 fe1a847c3a..ca35bee7ea 100644 --- a/big_tests/tests/graphql_http_upload_SUITE.erl +++ b/big_tests/tests/graphql_http_upload_SUITE.erl @@ -3,9 +3,9 @@ -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_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. @@ -157,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 @@ -191,8 +218,14 @@ 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), + 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 de4f8332e4..011d81d35e 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_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,34 +21,62 @@ 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, + admin_try_flush_user_bin_nonexistent_domain, admin_flush_domain_bin, admin_try_flush_nonexistent_domain_bin, admin_flush_global_bin, admin_flush_global_bin_after_days, admin_try_flush_nonexistent_host_type_bin]. +domain_admin_inbox_tests() -> + [admin_flush_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]. + +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). @@ -54,15 +84,42 @@ 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(user_inbox, Config) -> - graphql_helper:init_user(Config). +init_per_group(domain_admin_inbox, Config) -> + ensure_inbox_started(), + graphql_helper:init_domain_admin_handler(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. -end_per_group(_, _) -> - graphql_helper:clean(). +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(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). @@ -86,6 +143,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). @@ -99,6 +167,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). @@ -125,27 +198,53 @@ 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">>), ?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) -> + 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(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)). + %% User test cases user_flush_own_bin(Config) -> @@ -159,6 +258,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) -> diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 928617daa2..f208fd243a 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_ok_value/2, get_err_msg/1, get_err_code/1, get_unauthorized/1, + get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). @@ -13,6 +14,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(). @@ -20,24 +22,54 @@ suite() -> all() -> [{group, user}, {group, admin_http}, - {group, admin_cli}]. + {group, admin_cli}, + {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()}, - {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_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()}]. + +user_groups() -> + [{group, user_last}, + {group, user_last_not_configured}]. admin_groups() -> + [{group, admin_last_configured}, + {group, admin_last_not_configured_group}]. + +admin_last_configured() -> [{group, admin_last}, {group, admin_old_users}]. +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, 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, @@ -57,17 +89,42 @@ 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_set_user_last_no_permission, + admin_get_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]. + +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]. + +admin_last_not_configured() -> + [admin_set_last_not_configured, + admin_get_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(), 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). + 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), @@ -79,14 +136,47 @@ 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) -> + configure_last(Config), + graphql_helper:init_domain_admin_handler(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_old_users, Config) -> +init_per_group(admin_last_configured, Config) -> + configure_last(Config); +init_per_group(admin_last_not_configured_group, Config) -> + stop_last(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. + end; +init_per_group(admin_last_not_configured, Config) -> + Config. + +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 -> @@ -101,7 +191,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 +204,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) -> @@ -215,7 +311,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). @@ -255,7 +351,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). @@ -290,6 +386,76 @@ 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_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(?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) -> + 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) -> + 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], + 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) -> @@ -331,6 +497,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) -> diff --git a/big_tests/tests/graphql_metric_SUITE.erl b/big_tests/tests/graphql_metric_SUITE.erl index 2c9b6d203c..a144d158ba 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 96fa3f96c2..78297a4c0e 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, get_not_loaded/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -14,14 +15,28 @@ 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, @@ -63,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_affiliations_muc_not_configured]. + admin_muc_tests() -> [admin_create_and_delete_room, admin_try_create_instant_room_with_nonexistent_domain, @@ -101,17 +132,76 @@ 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, + admin_try_delete_nonexistent_room, + domain_admin_create_and_delete_room_no_permission, + admin_list_rooms, + domain_admin_list_rooms_no_permission, + admin_list_room_users, + domain_admin_list_room_users_no_permission, + admin_change_room_config, + domain_admin_change_room_config_no_permission, + admin_get_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, + 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_get_room_messages_no_permission, + admin_set_user_affiliation, + domain_admin_set_user_affiliation_no_permission, + admin_set_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_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(), - mongoose_helper:ensure_muc_clean(), - Config5. + Config6. end_per_suite(Config) -> escalus_fresh:clean(), @@ -120,15 +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(user_muc, Config) -> +init_per_group(domain_admin_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, @@ -156,6 +273,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) -> @@ -582,6 +700,242 @@ 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) -> + RoomJID = jid:make_bare(<<"unknown">>, <<"unknown">>), + get_unauthorized(delete_room(RoomJID, null, 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(), + ExternalDomain = domain_helper:secondary_domain(), + UnknownDomain = <<"unknown">>, + MUCServer = muc_helper:muc_host(), + 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)). + +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_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_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_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_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_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_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_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_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_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_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 user_list_rooms(Config) -> @@ -1144,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_affiliations_muc_not_configured(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_list_room_affiliations_muc_not_configured_story/2). + +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). + %% Helpers assert_no_full_jid(Res) -> @@ -1231,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/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl index 8882059280..4b7d193064 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_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,14 +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()}]. + [{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, @@ -64,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, @@ -71,37 +99,85 @@ 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_get_room_config_non_existent_domain, + admin_blocking_list, + admin_blocking_list_errors + ]. + +domain_admin_muc_light_tests() -> + [admin_create_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, + 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_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_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, - admin_blocking_list + domain_admin_get_room_config_no_permission, + admin_blocking_list, + 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()} - | escalus:init_per_suite(Config2)]. + 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), + Config4 = ejabberd_node_utils:init(mim(), Config3), + escalus:init_per_suite(Config4). 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(), - Config1 = dynamic_modules:save_modules(HostType, Config), - Config2 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config1), - dynamic_modules:ensure_modules(HostType, required_modules(suite)), - Config2. - required_modules(_) -> MucLightOpts = mod_config(mod_muc_light, #{rooms_in_rosters => true, config_schema => custom_schema()}), @@ -115,15 +191,45 @@ 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(user_muc_light, Config) -> +init_per_group(domain_admin_muc_light, 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); +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(), + SecondaryHostType = domain_helper:secondary_host_type(), + dynamic_modules:ensure_modules(HostType, required_modules(suite)), + 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) -> + escalus_fresh:clean(). init_per_testcase(TC, Config) -> rest_helper:maybe_skip_mam_test_cases(TC, [user_get_room_messages, @@ -537,6 +643,219 @@ user_blocking_list_story(Config, Alice, Bob) -> <<"entity">> => RoomBin}], get_ok_value(?GET_BLOCKING_LIST_PATH, Res5)). +%% Domain admin test cases + +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_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">>, + % Try with a non-existent domain + 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_no_permission(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun domain_admin_change_room_config_no_permission_story/3). + +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(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), + RoomJIDBin = jid:to_binary(RoomJID), + Res = change_room_configuration(RoomJIDBin, AliceBin, RoomName, <<"subject">>, Config), + get_unauthorized(Res), + % Try to change the config with an external domain + Res2 = change_room_configuration( + make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), AliceBin, RoomName, <<"subject2">>, Config), + get_unauthorized(Res2). + +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, 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). + +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_kick_user_no_permission_story(Config, Alice, Bob) -> + AliceBin = escalus_client:short_jid(Alice), + BobBin = escalus_client:short_jid(Bob), + RoomName = <<"first room">>, + MUCServer = ?config(secondary_muc_light_host, Config), + {ok, #{jid := #jid{luser = RoomID} = RoomJID}} = + create_room(MUCServer, RoomName, <<"subject">>, AliceBin), + {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 + Res2 = kick_user(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), BobBin, Config), + get_unauthorized(Res2). + +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_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">>, + MsgBody = <<"Hello there!">>, + {ok, #{jid := RoomJID}} = create_room(MUCServer, RoomName, <<"subject">>, AliceBisBin), + {ok, _} = invite_user(RoomJID, AliceBisBin, BobBin), + % Try with a non-existent domain + 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_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_get_room_messages_no_permission_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + MUCServer = ?config(secondary_muc_light_host, Config), + RoomName = <<"first room">>, + 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 = get_room_messages(make_bare_jid(RoomID, ?UNKNOWN_DOMAIN), Limit, null, Config), + get_unauthorized(Res2). + +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_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), + 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 = 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). + +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_no_permission_story(Config, AliceBis) -> + AliceBisBin = escalus_client:full_jid(AliceBis), + InvalidUser = make_bare_jid(?UNKNOWN, ?UNKNOWN_DOMAIN), + % 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 admin_blocking_list(Config) -> @@ -560,13 +879,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). @@ -613,7 +933,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), @@ -682,25 +1004,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). @@ -754,12 +1088,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). @@ -844,10 +1188,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). @@ -865,10 +1220,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). @@ -882,12 +1238,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). @@ -901,7 +1266,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">> => <<>>}, @@ -910,15 +1275,221 @@ 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">>)). + +%% 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/big_tests/tests/graphql_offline_SUITE.erl b/big_tests/tests/graphql_offline_SUITE.erl index 040bd94ef9..def5d6253b 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, get_not_loaded/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_permission_test, + domain_admin_delete_old_messages_no_permission_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,25 @@ 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(admin_offline, Config) -> +init_per_group(domain_admin, Config) -> + graphql_helper:init_domain_admin_handler(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), 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. @@ -129,11 +157,21 @@ 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 + +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_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 f66aea31be..530d9a78d1 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, get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("exml/include/exml.hrl"). @@ -13,31 +13,59 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_private}, {group, admin_private_http}, {group, admin_private_cli}]. + [{group, user}, + {group, domain_admin_private}, + {group, admin_http}, + {group, admin_cli}]. groups() -> - [{user_private, [], user_private_tests()}, - {admin_private_http, [], admin_private_tests()}, - {admin_private_cli, [], admin_private_tests()}]. + [{user, [], user_groups()}, + {domain_admin_private, [], domain_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, + domain_admin_user_set_private_no_permission, + domain_admin_user_get_private_no_permission]. + admin_private_tests() -> [admin_set_private, admin_get_private, 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) -> @@ -51,16 +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_private, Config) -> - graphql_helper:init_user(Config). +init_per_group(user, Config) -> + graphql_helper:init_user(Config); +init_per_group(domain_admin_private, 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). @@ -101,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) -> @@ -143,6 +213,48 @@ private_input() -> attrs = [{<<"xmlns">>, "alice:private:ns"}], children = [{xmlcdata, <<"DATA">>}]}. +% 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}], + 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(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 user_set_private(User, ElementString, Config) -> diff --git a/big_tests/tests/graphql_roster_SUITE.erl b/big_tests/tests/graphql_roster_SUITE.erl index 508d62bca9..5cf2da2042 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, @@ -64,10 +69,37 @@ 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, + 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]. + 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, + 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), @@ -291,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). @@ -317,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). @@ -498,6 +564,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 +634,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) -> @@ -524,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], @@ -532,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), @@ -561,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 @@ -594,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/big_tests/tests/graphql_server_SUITE.erl b/big_tests/tests/graphql_server_SUITE.erl new file mode 100644 index 0000000000..6745b79c76 --- /dev/null +++ b/big_tests/tests/graphql_server_SUITE.erl @@ -0,0 +1,249 @@ +-module(graphql_server_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [is_sm_distributed/0, + mim/0, mim2/0, mim3/0, + 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, execute_command/5]). + +-include_lib("eunit/include/eunit.hrl"). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [%{group, admin_http}, % http is not supported for the server category + {group, admin_cli}]. + +groups() -> + [{admin_http, [], admin_groups()}, + {admin_cli, [], admin_groups()}, + {server_tests, [], admin_tests()}, + {clustering_tests, [], clustering_tests()}]. + +admin_groups() -> + [{group, server_tests}, + {group, clustering_tests}]. + +admin_tests() -> + [get_cookie_test, + set_and_get_loglevel_test, + get_status_test]. + +clustering_tests() -> + [join_successful, + leave_successful, + join_unsuccessful, + leave_but_no_cluster, + join_twice, + remove_dead_from_cluster, + remove_alive_from_cluster, + remove_node_test, + stop_node_test]. + +init_per_suite(Config) -> + Config1 = dynamic_modules:save_modules(host_type(), Config), + 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", + 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_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(). + +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 -> + 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 -> + 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). + +get_cookie_test(Config) -> + Result = get_ok_value([data, server, getCookie], get_cookie(Config)), + ?assert(is_binary(Result)). + +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), + 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)), + ?assertEqual(<<"RUNNING">>, maps:get(<<"statusCode">>, Result)), + ?assert(is_binary(maps:get(<<"message">>, Result))). + +join_successful(Config) -> + #{node := Node2} = RPCSpec2 = mim2(), + leave_cluster(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), + get_ok_value([], 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(), + get_err_code(leave_cluster(Config)), + distributed_helper:verify_result(Node2, remove). + +join_twice(Config) -> + #{node := Node2} = RPCSpec2 = mim2(), + 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), + timer:sleep(1000), + 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)), + 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(<<"MongooseIM node removed from the Mnesia schema">>, Value). + +stop_node_test(Config) -> + #{node := Node3Nodename} = mim3(), + get_ok_value([data, server, stop], stop_node(Node3Nodename, Config)), + Timeout = timer:seconds(3), + 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). + +%----------------------------------------------------------------------- +% 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)). + +get_cookie(Config) -> + execute_command(<<"server">>, <<"getCookie">>, #{}, 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). + +get_status(Node, Config) -> + execute_command(Node, <<"server">>, <<"status">>, #{}, Config). + +join_cluster(Node, Config) -> + execute_command(<<"server">>, <<"joinCluster">>, #{<<"node">> => 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). + +remove_node(Node, Config) -> + execute_command(Node, <<"server">>, <<"removeNode">>, #{<<"node">> => Node}, Config). diff --git a/big_tests/tests/graphql_session_SUITE.erl b/big_tests/tests/graphql_session_SUITE.erl index de864e574b..f7ff33905e 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,24 @@ 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, + 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, + domain_admin_set_presence_no_permission]. + init_per_suite(Config) -> Config1 = ejabberd_node_utils:init(mim(), Config), Config2 = escalus:init_per_suite(Config1), @@ -54,11 +74,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 +129,156 @@ 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_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). + +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 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), + Res4 = list_users_with_status(null, DndStatus, Config), + get_unauthorized(Res4), + % List users with dnd status for a domain + Res5 = list_users_with_status(domain_helper:domain(), DndStatus, Config), + StatusUsers2 = get_ok_value(Path, Res5), + ?assertEqual(1, length(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}], + 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 d11df13a15..ede9e54a6b 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,18 @@ 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_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_no_permission] + ++ admin_get_last_messages_cases(). + user_stanza_cases() -> [user_send_message, user_send_message_without_from, @@ -68,6 +83,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). @@ -194,6 +211,57 @@ 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). + +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_stats_SUITE.erl b/big_tests/tests/graphql_stats_SUITE.erl index 5af5dcbf92..9d17f95335 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 fc3641d185..aa49c37bb0 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, get_not_loaded/1]). -include_lib("eunit/include/eunit.hrl"). @@ -13,19 +13,44 @@ suite() -> all() -> [{group, user}, + {group, domain_admin}, {group, admin_http}, {group, admin_cli}]. groups() -> - [{user, [], user_tests()}, - {admin_http, [], admin_tests()}, - {admin_cli, [], admin_tests()}]. + [{user, [], user_groups()}, + {domain_admin, domain_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, + domain_admin_revoke_token_no_permission_test, + admin_revoke_token_no_token_test, + admin_revoke_token_test]. + admin_tests() -> [admin_request_token_test, admin_request_token_no_user_test, @@ -33,12 +58,15 @@ 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 -> 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 -> @@ -65,11 +93,34 @@ 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) -> + 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) -> @@ -106,6 +157,50 @@ 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) -> + 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 admin_request_token_test(Config) -> @@ -142,6 +237,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/big_tests/tests/graphql_vcard_SUITE.erl b/big_tests/tests/graphql_vcard_SUITE.erl index ed506a500f..1781c02607 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, get_not_loaded/1, get_err_code/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -13,14 +14,28 @@ suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). all() -> - [{group, user_vcard}, - {group, admin_vcard_http}, - {group, admin_vcard_cli}]. + [{group, user}, + {group, domain_admin_vcard}, + {group, admin_http}, + {group, admin_cli}]. groups() -> - [{user_vcard, [], user_vcard_tests()}, - {admin_vcard_http, [], admin_vcard_tests()}, - {admin_vcard_cli, [], admin_vcard_tests()}]. + [{user, [], user_groups()}, + {domain_admin_vcard, [], domain_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, @@ -30,6 +45,21 @@ 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, + 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, @@ -38,31 +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(user_vcard, Config) -> - graphql_helper:init_user(Config). - +init_per_group(domain_admin_vcard, 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). @@ -141,6 +202,59 @@ 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) -> + 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, AliceBis) -> + Vcard = complete_vcard_input(), + Result = set_vcard(Vcard, user_to_bin(AliceBis), Config), + get_unauthorized(Result). + %% Admin test cases admin_set_vcard(Config) -> @@ -198,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/big_tests/tests/inbox_SUITE.erl b/big_tests/tests/inbox_SUITE.erl index 0259eada6b..8a2d907ce0 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), diff --git a/big_tests/tests/inbox_helper.erl b/big_tests/tests/inbox_helper.erl index 32582acedb..f8963d032f 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() -> diff --git a/big_tests/tests/metrics_api_SUITE.erl b/big_tests/tests/metrics_api_SUITE.erl index 27681d968c..b07805052e 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"). @@ -50,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) -> @@ -90,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); @@ -245,10 +254,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 +264,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 +272,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 +287,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 +295,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 +303,39 @@ 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), - 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), - 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), - 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}). diff --git a/big_tests/tests/muc_helper.erl b/big_tests/tests/muc_helper.erl index f064220b8b..75c757b1db 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, @@ -71,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/big_tests/tests/muc_http_api_SUITE.erl b/big_tests/tests/muc_http_api_SUITE.erl index 0e84092534..2b49c7c51a 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 @@ -37,9 +38,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() -> [ @@ -56,8 +56,10 @@ complex() -> ]. failure_response() -> - [failed_invites, - failed_messages]. + [room_creation_errors, + invite_errors, + kick_user_errors, + message_errors]. %%-------------------------------------------------------------------- %% Init & teardown @@ -127,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), @@ -274,56 +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), - % non-existing 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), - ok - end). - -failed_messages(Config) -> - escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - Name = set_up_room(Config, Alice), - % non-existing room - 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), - 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). - 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/muc_light_http_api_SUITE.erl b/big_tests/tests/muc_light_http_api_SUITE.erl index 9c3bc1c9fa..653af74e85 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 %%-------------------------------------------------------------------- diff --git a/big_tests/tests/rest_SUITE.erl b/big_tests/tests/rest_SUITE.erl index a37b7861b9..811048981a 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} @@ -64,35 +58,47 @@ 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]}, - {dynamic_module, [], [stop_start_command_module]}]. + {roster, [parallel], roster_test_cases()} + ]. auth_test_cases() -> [auth_passes_correct_creds, auth_fails_incorrect_creds]. blank_auth_testcases() -> - [auth_always_passes_blank_creds]. + [auth_passes_without_creds, + auth_fails_with_creds]. test_cases() -> - [commands_are_listed, - non_existent_command_returns404, + [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, 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, - types_are_checked_separately_for_args_and_return + 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(). @@ -125,61 +131,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], + MAMTestCases = [messages_are_archived, message_archive_errors, 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 +148,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">>}), +auth_passes_without_creds(_Config) -> % 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()])). + +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">>). @@ -225,6 +168,16 @@ 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">>). + user_can_be_registered_and_removed(_Config) -> % list users {?OK, Lusers} = gett(admin, path("users")), @@ -240,14 +193,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 @@ -273,6 +237,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), @@ -282,29 +253,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 jid:", _/binary>>} = send_message_bin(AliceJID, <<"@noway">>), - {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = 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, <<"both from and to are required">>} = Res1, - Res2 = send_flawed_stanza(malformed_xml, Alice, Bob), - {?BAD_REQUEST, <<"Malformed stanza: \"expected >\"">>} = 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), @@ -343,6 +344,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)), @@ -394,15 +409,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( @@ -436,6 +464,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), @@ -526,69 +555,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", _/binary>>} = post(admin, AlicePath, #{jid => <<"@invalidjid">>}), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = 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">>}), - PutPathB = lists:flatten(["/contacts/@invalid_jid/", BobS]), - {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = 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 jid", _/binary>>} = 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}), - 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)), @@ -603,39 +638,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), @@ -665,20 +686,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) -> diff --git a/big_tests/tests/rest_helper.erl b/big_tests/tests/rest_helper.erl index 3a9ffd678e..19bd18fd1f 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/big_tests/tests/service_domain_db_SUITE.erl b/big_tests/tests/service_domain_db_SUITE.erl index 54f667e150..23aca71251 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]). @@ -156,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, @@ -209,11 +202,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 +213,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(); @@ -370,7 +358,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 +375,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 +388,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 +401,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 +516,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 +530,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 +707,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 +812,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) -> @@ -841,8 +829,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,204 +838,187 @@ 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) -> 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) -> - {{<<"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_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">>}, - {[{<<"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) -> @@ -1143,7 +1113,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/big_tests/tests/users_api_SUITE.erl b/big_tests/tests/users_api_SUITE.erl deleted file mode 100644 index 9c3599217e..0000000000 --- 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. diff --git a/doc/configuration/Modules.md b/doc/configuration/Modules.md index e43136acb7..5b17c9fcca 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 99120ece6b..6adc440ab1 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 5d7cd970b2..f492e0bbb3 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 957cac703d..0d90930a0e 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 cce0c7ab94..82ef8456f2 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 c299bfccdb..4fac9218b6 100644 --- a/doc/migrations/5.1.0_6.0.0.md +++ b/doc/migrations/5.1.0_6.0.0.md @@ -1,17 +1,31 @@ -## 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. + +## 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/doc/modules/mod_commands.md b/doc/modules/mod_commands.md deleted file mode 100644 index 86a28b5f0c..0000000000 --- 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 45670a7fd1..1ad70000e4 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 18713594bd..0000000000 --- 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_mam.md b/doc/modules/mod_mam.md index 8ff5bd5c64..4d1a7b6cf9 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/doc/modules/mod_muc_commands.md b/doc/modules/mod_muc_commands.md deleted file mode 100644 index 4ef1778df2..0000000000 --- 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 68eb985b52..0000000000 --- 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 377972bd40..ac643f6f61 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 4517d3bc79..1120dc7fc1 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 4173c7c292..12238c84d3 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 c3f52153a5..0000000000 --- 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 887ee848d0..0000000000 --- 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 57c58b9d61..0000000000 --- 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 e69de29bb2..0000000000 diff --git a/doc/swagger/index.html b/doc/swagger/index.html index 13594fbbeb..fdb2f5f578 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 e57eef6ad6..1b967e3d1d 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 73beca05ad..dc30d25008 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/priv/graphql/schemas/admin/account.gql b/priv/graphql/schemas/admin/account.gql index 6856001d00..bb2bdeeec7 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_auth_status.gql b/priv/graphql/schemas/admin/admin_auth_status.gql index d59db22647..799ab3aa0a 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/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 60a39b7cb7..6209c281d0 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,24 +20,26 @@ 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 info and management" + server: ServerAdminQuery } """ @@ -47,32 +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 info and management" + server: ServerAdminMutation } diff --git a/priv/graphql/schemas/admin/http_upload.gql b/priv/graphql/schemas/admin/http_upload.gql index 6690cc9f7f..fccffa1dca 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/admin/last.gql b/priv/graphql/schemas/admin/last.gql index 30e6c0a376..ac003c1709 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 27b8a20673..db2772d1b7 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 appropriate 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 + "TCP/IP connection statistics from the 'inet' module" merged_inet_stats + "Metrics of the relational database management system" rdbms_stats + "Metrics of the virtual machine memory" vm_stats_memory + "Information about virtual 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 e08b17ad10..1a859a2353 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) } @@ -40,17 +43,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 an integer" type MnesiaIntResponse { + "Result as an integer" result: Int + "Result's key" key: String } diff --git a/priv/graphql/schemas/admin/muc.gql b/priv/graphql/schemas/admin/muc.gql index 53763c2784..e74c57e856 100644 --- a/priv/graphql/schemas/admin/muc.gql +++ b/priv/graphql/schemas/admin/muc.gql @@ -1,59 +1,61 @@ """ 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" + #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" 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" + #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" 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/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql index 37b5cd8fa2..ae148a6e63 100644 --- a/priv/graphql/schemas/admin/muc_light.gql +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -1,47 +1,48 @@ """ Allow admin to manage Multi-User Chat Light rooms. """ -type MUCLightAdminMutation @protected{ +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" 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"]) - "Set the user blocking list" + @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"]) - "Get the user blocking list" + @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/admin/offline.gql b/priv/graphql/schemas/admin/offline.gql index 9e1b15a184..ca212fd5f2 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{ - "Allow admin to delete offline messages whose date has expired" - deleteExpiredMessages(domain: String!): String +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"]) - "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"]) + @protected(type: DOMAIN, args: ["domain"]) @use(arg: "domain") } diff --git a/priv/graphql/schemas/admin/private.gql b/priv/graphql/schemas/admin/private.gql index ec5ce5857d..8413237d2d 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" +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 user's private +Allow admin to get the user's private data """ -type PrivateAdminQuery @protected { - "Get user's private" +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/admin/roster.gql b/priv/graphql/schemas/admin/roster.gql index a170fb5e76..5b68d2c7bb 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" @@ -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"]) } @@ -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 new file mode 100644 index 0000000000..245a0f32a5 --- /dev/null +++ b/priv/graphql/schemas/admin/server.gql @@ -0,0 +1,81 @@ +""" +Allow admin to acquire data about the node +""" +type ServerAdminQuery @protected{ + "Get the status of the server" + status: Status + @protected(type: GLOBAL) + "Get MongooseIM node's current LogLevel" + 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 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 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: String + @protected(type: GLOBAL) + "Stop MongooseIM node" + stop: String + @protected(type: GLOBAL) + "Remove a MongooseIM node from Mnesia clustering config" + removeNode(node: String!): String + @protected(type: GLOBAL) + "Set MongooseIM node's LogLevel" + setLoglevel(level: LogLevel!): String + @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/admin/session.gql b/priv/graphql/schemas/admin/session.gql index 690dd7b6cf..3e6724c97d 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!] diff --git a/priv/graphql/schemas/admin/stanza.gql b/priv/graphql/schemas/admin/stanza.gql index 9a562ed375..151c1629b8 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 c0658f09d5..a0dfecd78b 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 366d4623c7..8c38935fca 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 { - "Request auth token for an user" + type TokenAdminMutation @protected @use(modules: ["mod_auth_token"]){ + "Request auth token for a user" requestToken(user: JID!): Token - @protected(type: DOMAIN, args: ["user"]) - "Revoke any tokens for an 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/admin/vcard.gql b/priv/graphql/schemas/admin/vcard.gql index 1eb788a925..0234004f19 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 user's vcard +Allow admin to get the user's vcard """ -type VcardAdminQuery @protected{ - "Get user's vcard" +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/global/domain.gql b/priv/graphql/schemas/global/domain.gql index 37fb23994a..19443fcd2e 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/priv/graphql/schemas/global/muc.gql b/priv/graphql/schemas/global/muc.gql index 16de7c3e35..6448258c6d 100644 --- a/priv/graphql/schemas/global/muc.gql +++ b/priv/graphql/schemas/global/muc.gql @@ -1,87 +1,159 @@ +"User affiliation to a specific room" enum MUCAffiliation{ + "The user is the owner of the room" OWNER + "The user has an administrative role" ADMIN + "The user is a member of the room" 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 visitor" 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 the 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 the room's subject?" allowChangeSubject: Boolean!, + "Allow to query users?" allowQueryUsers: Boolean!, + "Allow private messages?" allowPrivateMessages: Boolean!, + "Allow visitor status?" 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 status?" 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 a89fe1f539..8c03627f31 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 b39f4c2fae..9dc922c60e 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 4ee27eb651..0c06fdfc9d 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 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" }]) +"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 0000000000..d782ce3493 --- /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 1df83ce3d1..22eb60c9c0 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{ + "Stanza id" id: ID } diff --git a/priv/graphql/schemas/global/token.gql b/priv/graphql/schemas/global/token.gql index b2af0f7f7a..3b9ef7b3ab 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 30810d747b..b6a5497989 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 } diff --git a/priv/graphql/schemas/user/http_upload.gql b/priv/graphql/schemas/user/http_upload.gql index 665a30c2d2..3a4c141eea 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/priv/graphql/schemas/user/muc.gql b/priv/graphql/schemas/user/muc.gql index 7ec7a4ef97..1430afbe9c 100644 --- a/priv/graphql/schemas/user/muc.gql +++ b/priv/graphql/schemas/user/muc.gql @@ -1,43 +1,45 @@ """ 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" + #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 + 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" + #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 + 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") } diff --git a/priv/graphql/schemas/user/muc_light.gql b/priv/graphql/schemas/user/muc_light.gql index 89f74b062b..3263fa692f 100644 --- a/priv/graphql/schemas/user/muc_light.gql +++ b/priv/graphql/schemas/user/muc_light.gql @@ -1,35 +1,36 @@ """ 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" + #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 + 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/priv/graphql/schemas/user/private.gql b/priv/graphql/schemas/user/private.gql index 358290e8df..f8f2d59165 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/priv/graphql/schemas/user/token.gql b/priv/graphql/schemas/user/token.gql index f8beed436e..6f7e7bab1c 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 } diff --git a/priv/graphql/schemas/user/vcard.gql b/priv/graphql/schemas/user/vcard.gql index 166468ea52..c9fe960c26 100644 --- a/priv/graphql/schemas/user/vcard.gql +++ b/priv/graphql/schemas/user/vcard.gql @@ -1,15 +1,17 @@ """ 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 + #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") } 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 0000000000..d7bd908bf1 --- /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 0000000000..a50f0f671d --- /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 0000000000..bbe33fa77d --- /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 69974340df..351939358f 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 78a428c0b4..700889f8cd 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 335b7d6290..d0525f57c8 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/rebar.config b/rebar.config index 5fcfc46d5c..4416a2a6ca 100644 --- a/rebar.config +++ b/rebar.config @@ -164,19 +164,19 @@ ]}. {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 - {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 8db4c7981d..b6e34e8d40 100644 --- a/rel/fed1.vars-toml.config +++ b/rel/fed1.vars-toml.config @@ -1,15 +1,17 @@ +%% 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_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}. {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. @@ -37,32 +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_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 }}"}. -{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/files/mongooseim.toml b/rel/files/mongooseim.toml index 84d3fec162..e73075e38b 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}}} @@ -56,11 +60,7 @@ transport.num_acceptors = 10 transport.max_connections = 1024 - [[listen.http.handlers.mongoose_api_admin]] - host = "localhost" - path = "/api" - - [[listen.http.handlers.mongoose_domain_handler]] + [[listen.http.handlers.mongoose_admin_api]] host = "localhost" path = "/api" @@ -117,17 +117,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}} @@ -217,16 +206,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}}} @@ -374,7 +357,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}} diff --git a/rel/mim1.vars-toml.config b/rel/mim1.vars-toml.config index 2db63aee1d..73e7cd99c3 100644 --- a/rel/mim1.vars-toml.config +++ b/rel/mim1.vars-toml.config @@ -1,8 +1,21 @@ +%% 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\""}. @@ -39,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]] @@ -77,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 a1a08d2dc1..d02ab74982 100644 --- a/rel/mim2.vars-toml.config +++ b/rel/mim2.vars-toml.config @@ -1,11 +1,13 @@ -{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}. -{http_api_old_endpoint_port, 5289}. {http_api_endpoint_port, 8090}. {http_api_client_endpoint_port, 8091}. {service_port, 8899}. @@ -19,20 +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_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 }}"}. {tls_config, "tls.verify_mode = \"none\" tls.certfile = \"priv/ssl/fake_server.pem\" @@ -69,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 422d2de57f..eab4354adc 100644 --- a/rel/mim3.vars-toml.config +++ b/rel/mim3.vars-toml.config @@ -1,17 +1,22 @@ +%% 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_old_endpoint_port, 5292}. -{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\""}. @@ -19,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\" @@ -40,24 +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_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 }}"}. - {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 0000000000..666ef865a3 --- /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 8281f21720..4b5a4e4fea 100644 --- a/rel/reg1.vars-toml.config +++ b/rel/reg1.vars-toml.config @@ -1,16 +1,18 @@ +%% 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_old_endpoint_port, 5273}. -{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. @@ -26,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\" @@ -38,25 +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_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 }}"}. {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.in b/rel/vars-toml.config similarity index 51% rename from rel/vars-toml.config.in rename to rel/vars-toml.config index 62b2d59b2e..d17f75d1fc 100644 --- a/rel/vars-toml.config.in +++ b/rel/vars-toml.config @@ -1,47 +1,11 @@ -{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\" 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\" @@ -49,13 +13,14 @@ {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"}. -%% 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)"}. diff --git a/src/admin_extra/service_admin_extra_node.erl b/src/admin_extra/service_admin_extra_node.erl index a91f68f10d..53162c65d0 100644 --- a/src/admin_extra/service_admin_extra_node.erl +++ b/src/admin_extra/service_admin_extra_node.erl @@ -26,13 +26,9 @@ -module(service_admin_extra_node). -author('badlop@process-one.net'). --export([commands/0, - get_cookie/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"). @@ -45,29 +41,12 @@ commands() -> [ #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", - module = ?MODULE, 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 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)), - ok. - diff --git a/src/async_pools/mongoose_aggregator_worker.erl b/src/async_pools/mongoose_aggregator_worker.erl index fcabb679a1..2d489062a7 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}}. @@ -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}. @@ -94,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)}; @@ -127,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, @@ -148,20 +150,48 @@ 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) -> + 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} -> {Value, NewAcc} = maps:take(Key, Acc), - State#state{async_request = make_async_request(Value, 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. -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; diff --git a/src/async_pools/mongoose_async_pools.erl b/src/async_pools/mongoose_async_pools.erl index adfd1ad73d..5aa7b124cc 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 047647ad35..d55379e5ab 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/src/auth/ejabberd_auth.erl b/src/auth/ejabberd_auth.erl index 6044d1b19e..8ab4df25dc 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], diff --git a/src/domain/mongoose_domain_api.erl b/src/domain/mongoose_domain_api.erl index f595bfa6d8..15c2076908 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]). @@ -34,10 +39,13 @@ -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(). +-type remove_domain_acc() :: #{failed := [module()]}. +-export_type([status/0, remove_domain_acc/0]). -spec init() -> ok | {error, term()}. init() -> @@ -66,27 +74,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()}}. @@ -238,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/domain/mongoose_domain_handler.erl b/src/domain/mongoose_domain_handler.erl deleted file mode 100644 index 9851a66814..0000000000 --- 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/domain/mongoose_domain_sql.erl b/src/domain/mongoose_domain_sql.erl index 56ba299439..7844d4a1f1 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,44 @@ 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 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]). -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 +82,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 +99,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 +155,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 +228,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 +326,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(), mongoose_domain_api: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) -> mongoose_domain_api: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(mongoose_domain_api: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/ejabberd_admin.erl b/src/ejabberd_admin.erl index dcb9f2e398..4efea10d2b 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -30,15 +30,12 @@ -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, remove_from_cluster/1]). -export([registrator_proc/1]). @@ -46,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]). @@ -72,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 = ?MODULE, function = status, + module = mongoose_server_api, function = status, args = [], result = {res, restuple}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", @@ -80,7 +77,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 = mongoose_server_api, function = get_loglevel, args = [], result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], @@ -159,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 = ?MODULE, 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 = ?MODULE, 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 = ?MODULE, function = remove_from_cluster, + module = mongoose_server_api, function = remove_from_cluster, args = [{node, string}], result = {res, restuple}} ]. @@ -221,70 +218,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), - 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. - --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 +330,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/ejabberd_app.erl b/src/ejabberd_app.erl index c41cadbb63..dfea351e79 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/ejabberd_cowboy.erl b/src/ejabberd_cowboy.erl index a390b52f77..6f81036230 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/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 67427231f8..da9b62c9bf 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) -> diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 0195f92ee1..565ea0dd6a 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([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/ejabberd_router.erl b/src/ejabberd_router.erl index 0c73243322..1cd0a92d44 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)}. diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index cdd9dc41a6..bb3a62086f 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(), diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 72956d29e6..dd0a71bd99 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/ejabberd_sm.erl b/src/ejabberd_sm.erl index d6a1c57c84..05a7a538fe 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, @@ -71,7 +68,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, @@ -81,10 +82,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"). @@ -295,40 +294,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:foreach(fun({_, Pid}) -> 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{luser = LUser, lserver = LServer}) -> Ss = ejabberd_sm_backend:get_sessions(LUser, LServer), @@ -483,10 +448,46 @@ terminate_session(Pid, Reason) -> %% 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 :: #{jid := jid:jid()}, + Extra :: map(). +disconnect_removed_user(Acc, #{jid := #jid{luser = User, lserver = Server}}, _) -> + lists:map(fun({_, Pid}) -> terminate_session(Pid, <<"User removed">>) end, + get_user_present_pids(User, Server)), + {ok, Acc}. %%==================================================================== %% gen_server callbacks @@ -505,18 +506,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} ]. %%-------------------------------------------------------------------- @@ -846,7 +848,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/event_pusher/mod_event_pusher_hook_translator.erl b/src/event_pusher/mod_event_pusher_hook_translator.erl index 525d5f5a3b..e0ef8b96f0 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,68 +34,85 @@ -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 -> - 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, - {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 - 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) -> + {ok, {From, To, Acc, Packet}}. + +-spec user_send_packet(Acc, Args, Extra) -> {ok, Acc} when + Acc :: mongoose_acc:t(), + Args :: map(), + Extra :: map(). +user_send_packet(Acc, _, _) -> + 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}. + +-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 %%-------------------------------------------------------------------- +-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 @@ -115,13 +129,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/event_pusher/mod_event_pusher_push.erl b/src/event_pusher/mod_event_pusher_push.erl index b6909a7c23..a065e3f340 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()) -> diff --git a/src/global_distrib/mod_global_distrib.erl b/src/global_distrib/mod_global_distrib.erl index 1cd5c60f4b..0c188baa75 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, _, _) -> {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_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 diff --git a/src/global_distrib/mod_global_distrib_bounce.erl b/src/global_distrib/mod_global_distrib_bounce.erl index c638c07f9b..7a65a4c89a 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/global_distrib/mod_global_distrib_disco.erl b/src/global_distrib/mod_global_distrib_disco.erl index 31e530b203..389feaf3e7 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/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index e6fa2b2e19..42feb23d2d 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 4b22993e15..faf446b2a2 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_domain_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_domain_admin_mutation.erl index 311cc7d8b8..1512503ce9 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 933fbecac7..1fca40edfa 100644 --- a/src/graphql/admin/mongoose_graphql_domain_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_domain_admin_query.erl @@ -13,9 +13,8 @@ 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, #domain{host_type = HostType, domain = Domain, - enabled = Enabled}}; + {ok, #{host_type := HostType, status := Status}} -> + {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/admin/mongoose_graphql_muc_light_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl index abe77a394a..6a2cd7a36b 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); diff --git a/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_roster_admin_mutation.erl index 3ec26854c1..c2728ab007 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}. 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 0000000000..e679c31a04 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_server_admin_mutation.erl @@ -0,0 +1,48 @@ +-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">>, #{<<"node">> := Node}) -> + case mongoose_server_api:join_cluster(binary_to_list(Node)) of + {mnesia_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 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 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(mongoose_server_api, stop, []), + {ok, "Stop scheduled"}; +execute(_Ctx, server, <<"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 new file mode 100644 index 0000000000..dc442b7e5a --- /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 mongoose_server_api:status() of + {ok, String} -> + {ok, #{<<"statusCode">> => <<"RUNNING">>, <<"message">> => String}}; + {_, String} -> + {ok, #{<<"statusCode">> => <<"NOT_RUNNING">>, <<"message">> => String}} + end; +execute(_Ctx, server, <<"getLoglevel">>, _) -> + mongoose_server_api:graphql_get_loglevel(); +execute(_Ctx, server, <<"getCookie">>, _) -> + {ok, mongoose_server_api:get_cookie()}. diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 87771d96ad..8d8f2fe9d8 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_domain.erl b/src/graphql/mongoose_graphql_domain.erl index f516fa800e..fd4e89690f 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 948184bada..27d4095e42 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) -> @@ -33,8 +35,11 @@ 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(<<"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) -> @@ -68,5 +73,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(<<"MetricType">>, Type) -> {ok, Type}; +output(<<"StatusCode">>, Code) -> {ok, Code}. diff --git a/src/graphql/mongoose_graphql_types.hrl b/src/graphql/mongoose_graphql_types.hrl index dc3de12de5..859cab7c41 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(), diff --git a/src/http_upload/mod_http_upload_api.erl b/src/http_upload/mod_http_upload_api.erl index 35acc2e0c1..e490d5993f 100644 --- a/src/http_upload/mod_http_upload_api.erl +++ b/src/http_upload/mod_http_upload_api.erl @@ -34,6 +34,7 @@ get_urls(Domain, Filename, Size, ContentType, Timeout) -> end. check_module_and_get_urls(HostType, Filename, Size, ContentType, Timeout) -> + %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 diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index bff15a8e4d..726ce0b84d 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -27,12 +27,12 @@ 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, - 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]). @@ -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 @@ -165,7 +167,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]}, @@ -258,18 +260,22 @@ 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()) -> 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 +552,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/inbox/mod_inbox_api.erl b/src/inbox/mod_inbox_api.erl index 32e7fc10a5..0a77d7cc2d 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/inbox/mod_inbox_commands.erl b/src/inbox/mod_inbox_commands.erl deleted file mode 100644 index 27b1e4022d..0000000000 --- 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/logger/mongoose_logs.erl b/src/logger/mongoose_logs.erl index 12955ae95b..1660f14d87 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/mam/mod_mam.erl b/src/mam/mod_mam.erl index ac1818c5af..8602bd90be 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/src/mam/mod_mam_muc_rdbms_arch.erl b/src/mam/mod_mam_muc_rdbms_arch.erl index e08248b929..07d84fe5c5 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"). %% ---------------------------------------------------------------------- @@ -54,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. @@ -108,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 * 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 (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 = ?">>), @@ -173,8 +180,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 +203,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). @@ -312,15 +311,34 @@ 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) -> + F = fun() -> + 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) -> SubHosts = get_subhosts(HostType, Domain), {atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> [remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] - end), - Acc. + 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]), diff --git a/src/mam/mod_mam_pm.erl b/src/mam/mod_mam_pm.erl index 6588bb2e20..dfc05f52ff 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,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/1, 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(). @@ -154,14 +153,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 +203,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 +682,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 +694,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. @@ -712,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) diff --git a/src/mam/mod_mam_rdbms_arch.erl b/src/mam/mod_mam_rdbms_arch.erl index f72d05c8ec..1e84beb2b7 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 * 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 " - "(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 = ?">>), @@ -197,8 +204,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 +233,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). @@ -341,15 +340,32 @@ 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) -> + F = fun() -> + 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), - Acc. + 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) -> diff --git a/src/mam/mod_mam_utils.erl b/src/mam/mod_mam_utils.erl index 83222a1f81..f9e8cc213f 100644 --- a/src/mam/mod_mam_utils.erl +++ b/src/mam/mod_mam_utils.erl @@ -90,7 +90,9 @@ %% Shared logic -export([check_result_for_policy_violation/2, - lookup/3]). + 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(), mam_iq:lookup_params(), @@ -1159,7 +1161,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 +1223,43 @@ 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). + +-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}. diff --git a/src/metrics/mongoose_api_metrics.erl b/src/metrics/mongoose_api_metrics.erl deleted file mode 100644 index f5ee51443b..0000000000 --- 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/metrics/mongoose_metrics.erl b/src/metrics/mongoose_metrics.erl index 37b01571aa..393a479267 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/mod_adhoc.erl b/src/mod_adhoc.erl index 6563e4da3c..a7b6f56c7e 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_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 cd361df04f..c9ab118e9f 100644 --- a/src/mod_amp.erl +++ b/src/mod_amp.erl @@ -11,20 +11,15 @@ -export([start/2, stop/1, supported_features/0]). -export([run_initial_check/3, check_packet/2, - disco_local_features/1, + disco_local_features/3, c2s_stream_features/3, - xmpp_send_element/2 - ]). + xmpp_send_element/2]). --ignore_xref([c2s_stream_features/3, - disco_local_features/1, - run_initial_check/2, - xmpp_send_element/2]). +-ignore_xref([c2s_stream_features/3, xmpp_send_element/2]). -include("amp.hrl"). -include("mongoose.hrl"). -include("jlib.hrl"). --include("ejabberd_c2s.hrl"). -define(AMP_FEATURE, #xmlel{name = <<"amp">>, attrs = [{<<"xmlns">>, ?NS_AMP_FEATURE}]}). @@ -33,26 +28,30 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, _Opts) -> - ejabberd_hooks:add(hooks(HostType)), - gen_hook:add_handlers(c2s_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)), - gen_hook:delete_handlers(c2s_hooks(HostType)). + gen_hook:delete_handlers(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)). -spec supported_features() -> [atom()]. supported_features() -> [dynamic_domains]. --spec hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. -hooks(HostType) -> +-spec legacy_hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. +legacy_hooks(HostType) -> [{c2s_stream_features, HostType, ?MODULE, c2s_stream_features, 50}, - {disco_local_features, HostType, ?MODULE, disco_local_features, 99}, {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}, {xmpp_send_element, HostType, ?MODULE, xmpp_send_element, 10}]. +-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99} + | c2s_hooks(HostType)]. + -spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:hook_fn()). c2s_hooks(HostType) -> [{c2s_preprocessing_hook, HostType, fun ?MODULE:run_initial_check/3, #{}, 10}]. @@ -76,12 +75,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()]. @@ -251,7 +253,7 @@ find(Pred, [H|T]) -> -spec xmpp_send_element(mongoose_acc:t(), exml:element()) -> mongoose_acc:t(). xmpp_send_element(Acc, _El) -> - Event = case mongoose_acc:get(c2s, send_result, Acc) of + Event = case mongoose_acc:get(c2s, send_result, undefined, Acc) of ok -> delivered; _ -> delivery_failed end, diff --git a/src/mod_auth_token.erl b/src/mod_auth_token.erl index 845a382485..22114b725b 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, + 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_bosh.erl b/src/mod_bosh.erl index b667b71ec7..271e775492 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_caps.erl b/src/mod_caps.erl index c91b44e735..7118f6c336 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_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 1547c495ce..8e228f174f 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([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_commands.erl b/src/mod_commands.erl deleted file mode 100644 index b4b5b10016..0000000000 --- a/src/mod_commands.erl +++ /dev/null @@ -1,466 +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}, - {security_policy, [user]}, - {args, [{caller, binary}]}, - {result, []} - ], - [ - {name, add_contact}, - {category, <<"contacts">>}, - {desc, <<"Add a contact to roster">>}, - {module, ?MODULE}, - {function, add_contact}, - {action, create}, - {security_policy, [user]}, - {args, [{caller, binary}, {jid, binary}]}, - {result, ok} - ], - [ - {name, subscription}, - {category, <<"contacts">>}, - {desc, <<"Send out a subscription request">>}, - {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}]}, - {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}, - {security_policy, [user]}, - {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}, - {security_policy, [user]}, - {args, [{caller, binary}, {jids, [binary]}]}, - {result, []} - ], - [ - {name, send_message}, - {category, <<"messages">>}, - {desc, <<"Send chat message from to">>}, - {module, ?MODULE}, - {function, send_message}, - {action, create}, - {security_policy, [user]}, - {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}, - {security_policy, [user]}, - {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}, - {security_policy, [user]}, - {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}, - {security_policy, [user]}, - {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_disco.erl b/src/mod_disco.erl index c362b7e8cf..eadefea3fd 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -47,11 +47,11 @@ 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, - 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"). @@ -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/mod_muc.erl b/src/mod_muc.erl index 89e45be22a..4174a1cc70 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_muc_api.erl b/src/mod_muc_api.erl index 468280429d..2193a8fc24 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/mod_muc_commands.erl b/src/mod_muc_commands.erl deleted file mode 100644 index acc299fdca..0000000000 --- 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/mod_private_api.erl b/src/mod_private_api.erl index ce6a1ca6f1..046eaddb29 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">>, diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 100bb67466..6e35d8bc7f 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/mod_roster_api.erl b/src/mod_roster_api.erl index cd38d58890..ddcf027f57 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.erl b/src/mongoose_admin_api/mongoose_admin_api.erl new file mode 100644 index 0000000000..1ec7b90f14 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api.erl @@ -0,0 +1,176 @@ +-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.hrl"). +-include("mongoose_config_spec.hrl"). + +-type handler_options() :: #{path := string(), username => binary(), password => binary(), + atom() => any()}. +-type req() :: cowboy_req:req(). +-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}, + <<"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(). +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)]. + +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 + +-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(#{}, AuthDetails) -> + AuthDetails =:= undefined. % Do not accept basic auth when not configured + +-spec parse_body(req()) -> #{atom() => jiffy:json_value()}. +parse_body(Req) -> + try + {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() | true}. +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} -> + error_response(ErrorType, Msg, Req, State) + end. + +-spec throw_error(error_type(), 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}. + +-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), #{}, jiffy:encode(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(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_contacts.erl b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl new file mode 100644 index 0000000000..f46d667c62 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_contacts.erl @@ -0,0 +1,168 @@ +-module(mongoose_admin_api_contacts). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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">>). 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 0000000000..affede9ca6 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_domain.erl @@ -0,0 +1,157 @@ +-module(mongoose_admin_api_domain). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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_admin_api/mongoose_admin_api_inbox.erl b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl new file mode 100644 index 0000000000..93fdebe863 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_inbox.erl @@ -0,0 +1,80 @@ +-module(mongoose_admin_api_inbox). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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. 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 0000000000..3455f78cd9 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_messages.erl @@ -0,0 +1,150 @@ +-module(mongoose_admin_api_messages). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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_metrics.erl b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl new file mode 100644 index 0000000000..544c5be00d --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_metrics.erl @@ -0,0 +1,148 @@ +-module(mongoose_admin_api_metrics). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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. 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 0000000000..6c8c384bee --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_muc.erl @@ -0,0 +1,167 @@ +-module(mongoose_admin_api_muc). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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. 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 0000000000..81e71cf23f --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_muc_light.erl @@ -0,0 +1,183 @@ +-module(mongoose_admin_api_muc_light). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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/mongoose_admin_api/mongoose_admin_api_sessions.erl b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl new file mode 100644 index 0000000000..2da78694eb --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_sessions.erl @@ -0,0 +1,76 @@ +-module(mongoose_admin_api_sessions). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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. 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 0000000000..d892f0da8a --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_stanzas.erl @@ -0,0 +1,93 @@ +-module(mongoose_admin_api_stanzas). + +-behaviour(mongoose_admin_api). +-export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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_admin_api/mongoose_admin_api_users.erl b/src/mongoose_admin_api/mongoose_admin_api_users.erl new file mode 100644 index 0000000000..05db920649 --- /dev/null +++ b/src/mongoose_admin_api/mongoose_admin_api_users.erl @@ -0,0 +1,131 @@ +-module(mongoose_admin_api_users). + +-behaviour(mongoose_admin_api). + -export([routes/1]). + +-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() :: 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, State) -> + mongoose_admin_api:init(Req, State). + +-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 = 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(denied, 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_password(#{password := Password}) -> Password; +get_password(#{}) -> throw_error(bad_request, <<"Missing password">>). + +get_new_password(#{newpass := Password}) -> Password; +get_new_password(#{}) -> throw_error(bad_request, <<"Missing new password">>). diff --git a/src/mongoose_api.erl b/src/mongoose_api.erl deleted file mode 100644 index b5c265d48c..0000000000 --- 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_admin.erl b/src/mongoose_api_admin.erl deleted file mode 100644 index 0419a3d8bf..0000000000 --- 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)}}. diff --git a/src/mongoose_api_client.erl b/src/mongoose_api_client.erl deleted file mode 100644 index 0f26c58dcd..0000000000 --- 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 a9a2c76fe8..ee9bd22b40 100644 --- a/src/mongoose_api_common.erl +++ b/src/mongoose_api_common.erl @@ -3,368 +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, - create_user_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 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()}, - 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({caller, _}, user) -> - ""; -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 %%-------------------------------------------------------------------- diff --git a/src/mongoose_api_format.erl b/src/mongoose_api_format.erl deleted file mode 100644 index 0b366f5381..0000000000 --- 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 4820a3cc52..0000000000 --- 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 8df2ae5da6..0000000000 --- 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 3952362aa5..0000000000 --- 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. diff --git a/src/mongoose_bin.erl b/src/mongoose_bin.erl index 94edb09768..db832e5565 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/mongoose_client_api/mongoose_client_api.erl b/src/mongoose_client_api/mongoose_client_api.erl index 53750134f2..29bd0fd426 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 8bcb4228b0..35755d8198 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]). @@ -16,9 +19,12 @@ -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(). + +-spec routes() -> mongoose_http_handler:routes(). +routes() -> + [{"/contacts/[:jid]", ?MODULE, #{}}]. trails() -> mongoose_client_api_contacts_doc:trails(). @@ -43,26 +49,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 +79,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_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}. -handle_single_deletion(_, undefined, Req, 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_single_deletion(ToDelete, Req, State = #{jid := CJid}) -> + ok = delete_contact(CJid, ToDelete), + {true, Req, State}. -handle_request_and_respond(_, undefined, _, _, 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 +131,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 +155,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}. diff --git a/src/mongoose_client_api/mongoose_client_api_messages.erl b/src/mongoose_client_api/mongoose_client_api_messages.erl index 493a68922a..6792e88fd2 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 fced7ea341..fa4773fbfd 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 5a062876c2..da76ac1a3d 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 7a9c3e1051..0921c768ca 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 4139a90ec6..528e58040c 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 6c34664e76..18b3e88df4 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, []), diff --git a/src/mongoose_commands.erl b/src/mongoose_commands.erl deleted file mode 100644 index 2b5098ded2..0000000000 --- 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 67eec58a56..1a83f2acb1 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, @@ -234,9 +232,9 @@ does_user_exist(HostType, Jid, RequestType) -> -spec remove_domain(HostType, Domain) -> Result when HostType :: binary(), Domain :: jid:lserver(), - Result :: ok. + Result :: mongoose_domain_api:remove_domain_acc(). 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) -> @@ -266,7 +264,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, []). + 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'. @@ -333,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 @@ -367,8 +358,12 @@ register_user(HostType, LServer, LUser) -> LUser :: jid:luser(), Result :: mongoose_acc:t(). remove_user(Acc, LServer, LUser) -> + 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), - 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(), @@ -387,8 +382,11 @@ resend_offline_messages_hook(Acc, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). rest_user_send_packet(Acc, From, To, Packet) -> + Params = #{}, + 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 @@ -432,14 +430,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 @@ -453,8 +443,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, or times out -spec user_ping_response(HostType, Acc, JID, Response, TDelta) -> Result when @@ -499,8 +492,11 @@ user_sent_keep_alive(HostType, JID) -> Packet :: exml:element(), Result :: mongoose_acc:t(). user_send_packet(Acc, From, To, Packet) -> + Params = #{}, + 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. @@ -680,9 +676,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(), @@ -691,8 +689,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(), @@ -756,9 +757,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(), @@ -831,9 +834,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. @@ -1293,7 +1299,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. @@ -1305,7 +1312,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. @@ -1499,8 +1506,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'. @@ -1509,7 +1519,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). %%%---------------------------------------------------------------------- diff --git a/src/mongoose_http_handler.erl b/src/mongoose_http_handler.erl index f526ccc48c..ce8a3bb8a1 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]). @@ -83,7 +83,5 @@ cowboy_host(Host) -> Host. configurable_handler_modules() -> [mod_websockets, mongoose_client_api, - mongoose_api, - mongoose_api_admin, - mongoose_domain_handler, + mongoose_admin_api, mongoose_graphql_cowboy_handler]. diff --git a/src/mongoose_lib.erl b/src/mongoose_lib.erl index 9625d4c841..e3340a0670 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_server_api.erl b/src/mongoose_server_api.erl new file mode 100644 index 0000000000..6a5637102d --- /dev/null +++ b/src/mongoose_server_api.erl @@ -0,0 +1,162 @@ +-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_node/1, set_loglevel/1, + graphql_get_loglevel/0]). + +-ignore_xref([get_loglevel/0]). + +-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 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, string()} | {invalid_level, string()}. +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', string()} | {'ok', string()}. +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, any()}. +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 MongooseIM 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 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 a MongooseIM 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), + ThisNode = node(), + case NodeList of + [ThisNode] -> + String = io_lib:format("The MongooseIM 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 MongooseIM node ~p has successfully left the cluster~n", [node()]), + {ok, String} + catch + 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 MongooseIM node ~p has been removed from the cluster~n", [DeadNode]), + {ok, String} + catch + error:{node_is_alive, 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]), + {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 MongooseIM node ~p~n. RPC Reason: ~p", [AliveNode, Reason]), + {rpc_error, String}; + ok -> + 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]), + {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, "MongooseIM node removed from the Mnesia schema"}. diff --git a/src/mongoose_stanza_api.erl b/src/mongoose_stanza_api.erl index 8c0774dd1a..fa3f09108d 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), diff --git a/src/mongoose_stanza_helper.erl b/src/mongoose_stanza_helper.erl index 85aaa1212b..ea1025c4f1 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(), diff --git a/src/muc_light/mod_muc_light.erl b/src/muc_light/mod_muc_light.erl index 4e9109b562..d1b509193d 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]). @@ -418,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) -> diff --git a/src/muc_light/mod_muc_light_api.erl b/src/muc_light/mod_muc_light_api.erl index 483fee4bf3..83ba0f2cbb 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_cache.erl b/src/muc_light/mod_muc_light_cache.erl index 13d9891037..29eb4f3317 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 -> 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 f0ad029ae3..0000000000 --- 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(), jid:literal_jid()) -> - 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}. diff --git a/src/offline/mod_offline_api.erl b/src/offline/mod_offline_api.erl index 7ea21bac8f..9c76fc5ee1 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. diff --git a/src/privacy/mod_blocking.erl b/src/privacy/mod_blocking.erl index cee9bb6fb8..7107c799b6 100644 --- a/src/privacy/mod_blocking.erl +++ b/src/privacy/mod_blocking.erl @@ -14,7 +14,7 @@ -export([ process_iq_get/5, process_iq_set/4, - disco_local_features/1 + disco_local_features/3 ]). -export([user_send_iq/3, @@ -31,13 +31,13 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) when is_map(Opts) -> - ejabberd_hooks:add(hooks(HostType)), - gen_hook:add_handlers(c2s_hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - gen_hook:delete_handlers(c2s_hooks(HostType)), - ejabberd_hooks:delete(hooks(HostType)). + gen_hook:delete_handlers(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)). deps(_HostType, Opts) -> [{mod_privacy, Opts, hard}]. @@ -49,11 +49,17 @@ supported_features() -> config_spec() -> mod_privacy:config_spec(). -hooks(HostType) -> - [{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, +-spec legacy_hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. +legacy_hooks(HostType) -> + [ {privacy_iq_get, HostType, ?MODULE, process_iq_get, 50}, {privacy_iq_set, HostType, ?MODULE, process_iq_set, 50}]. +-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 98} + | c2s_hooks(HostType)]. + -spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:hook_fn()). c2s_hooks(HostType) -> [ @@ -122,11 +128,12 @@ blocking_presence_to_contacts(Action, [Jid | JIDs], StateData) -> end, blocking_presence_to_contacts(Action, JIDs, StateData). --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. +-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/privacy/mod_privacy.erl b/src/privacy/mod_privacy.erl index 5cf542d383..c25ba74de4 100644 --- a/src/privacy/mod_privacy.erl +++ b/src/privacy/mod_privacy.erl @@ -31,11 +31,7 @@ -behaviour(mongoose_module_metrics). %% gen_mod --export([start/2]). --export([stop/1]). --export([deps/2]). --export([supported_features/0]). --export([config_spec/0]). +-export([start/2, stop/1, deps/2, config_spec/0, supported_features/0]). -export([process_iq_set/4, process_iq_get/5, @@ -44,15 +40,14 @@ remove_user/3, remove_domain/3, updated_list/3, - disco_local_features/1, - remove_unused_backend_opts/1, + remove_unused_backend_opts/1]). +-export([disco_local_features/3, user_send_message_or_presence/3, user_send_iq/3, user_receive_message/3, user_receive_presence/3, user_receive_iq/3, - foreign_event/3 - ]). + foreign_event/3]). %% to be used by mod_blocking only -export([do_user_send_iq/4]). @@ -62,7 +57,7 @@ -ignore_xref([ 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]). -include("jlib.hrl"). -include("mod_privacy.hrl"). @@ -85,13 +80,13 @@ -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. start(HostType, Opts) -> mod_privacy_backend:init(HostType, Opts), - ejabberd_hooks:add(hooks(HostType)), - gen_hook:add_handlers(c2s_hooks(HostType)). + ejabberd_hooks:add(legacy_hooks(HostType)), + gen_hook:add_handlers(hooks(HostType)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> - gen_hook:delete_handlers(c2s_hooks(HostType)), - ejabberd_hooks:delete(hooks(HostType)). + gen_hook:delete_handlers(hooks(HostType)), + ejabberd_hooks:delete(legacy_hooks(HostType)). deps(_HostType, _Opts) -> [{mod_presence, #{}, hard}]. @@ -127,9 +122,9 @@ remove_unused_backend_opts(Opts) -> maps:remove(riak, Opts). supported_features() -> [dynamic_domains]. -hooks(HostType) -> +-spec legacy_hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()]. +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}, @@ -140,6 +135,11 @@ hooks(HostType) -> {anonymous_purge_hook, HostType, ?MODULE, remove_user, 50} ]. +-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). +hooks(HostType) -> + [{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 98} + | c2s_hooks(HostType)]. + -spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:hook_fn()). c2s_hooks(HostType) -> [ @@ -372,11 +372,12 @@ privacy_list_push_iq(PrivListName) -> children = [#xmlel{name = <<"list">>, attrs = [{<<"name">>, PrivListName}]}]}]}. --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 aada409c59..c7a9a55268 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_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, diff --git a/src/rdbms/mongoose_rdbms.erl b/src/rdbms/mongoose_rdbms.erl index 1003f49242..1d0c810683 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); diff --git a/src/sha.erl b/src/sha.erl deleted file mode 100644 index dc9fb8bfa7..0000000000 --- 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)). - diff --git a/src/system_metrics/mongoose_system_metrics_collector.erl b/src/system_metrics/mongoose_system_metrics_collector.erl index 771e9ac0f3..4cca5f7e82 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_api_admin, mongoose_api_client, - mongoose_domain_handler, 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/src/vcard/mod_vcard_api.erl b/src/vcard/mod_vcard_api.erl index 172d3ca7cb..8f0401e813 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. diff --git a/test/batches_SUITE.erl b/test/batches_SUITE.erl index 14821a74e1..2700e89977 100644 --- a/test/batches_SUITE.erl +++ b/test/batches_SUITE.erl @@ -23,13 +23,16 @@ 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, 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 ]} ]. @@ -107,6 +110,49 @@ 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(1, _) -> timer:sleep(1), gen_server:send_request(Server, 1); + (_, _) -> drop 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, N}) || N <- lists:seq(1, 1000) ], + async_helper:wait_until( + fun() -> gen_server:call(Server, get_acc) end, 1). + +broadcast_reaches_all_workers(_) -> + {ok, Server} = gen_server:start_link(?MODULE, [], []), + 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( + 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 = (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), + 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, @@ -188,39 +234,31 @@ 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) ], ?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, [], []), - 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( - 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, [], []), - 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( @@ -233,6 +271,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}. diff --git a/test/commands_SUITE.erl b/test/commands_SUITE.erl deleted file mode 100644 index 68f49b7d3e..0000000000 --- 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 1f2d089e86..0000000000 --- a/test/commands_backend_SUITE.erl +++ /dev/null @@ -1,704 +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 -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -client_module() -> - mongoose_api_client. - -backend_module() -> - mongoose_api_admin. - -all() -> - [ - {group, simple_backend}, - {group, get_advanced_backend}, - {group, post_advanced_backend}, - {group, delete_advanced_backend}, - {group, simple_client} - ]. - -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 - ] - }, - {simple_client, [sequence], - [ - get_simple_client, - get_two_args_client, - get_bad_auth, - post_simple_client, - put_simple_client, - delete_simple_client - ] - } - ]. - -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). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% 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() -> - [ - [ - {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() ++ commands_client(). - - -%% 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. - -%% 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 -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -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({User, Password}) -> - Basic = list_to_binary("Basic " ++ base64:encode_to_string(User ++ ":"++ Password)), - [{<<"authorization">>, Basic}]; -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, {{_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()), - 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/common/config_parser_helper.erl b/test/common/config_parser_helper.erl index 6b4b351418..57b8df6490 100644 --- a/test/common/config_parser_helper.erl +++ b/test/common/config_parser_helper.erl @@ -170,11 +170,9 @@ 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">>}), - 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 => ""} @@ -185,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], @@ -197,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, @@ -682,10 +671,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_presence => #{}, mod_privacy => mod_config(mod_privacy, #{backend => rdbms}), @@ -911,11 +899,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) -> @@ -1087,16 +1075,14 @@ 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_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 6c9d15fb58..1926c03df1 100644 --- a/test/config_parser_SUITE.erl +++ b/test/config_parser_SUITE.erl @@ -97,10 +97,7 @@ groups() -> listen_http_handlers_bosh, listen_http_handlers_websockets, 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_admin_api, listen_http_handlers_graphql]}, {auth, [parallel], [auth_methods, auth_password, @@ -600,27 +597,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_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_api_client(_Config) -> - test_listen_http_handler(mongoose_api_client). - -listen_http_handlers_domain(_Config) -> - {P, T} = test_listen_http_handler(mongoose_domain_handler), +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) -> @@ -2048,6 +2035,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, @@ -2070,6 +2059,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})), diff --git a/test/config_parser_SUITE_data/mongooseim-pgsql.toml b/test/config_parser_SUITE_data/mongooseim-pgsql.toml index a599aab8c0..98c2a87a6e 100644 --- a/test/config_parser_SUITE_data/mongooseim-pgsql.toml +++ b/test/config_parser_SUITE_data/mongooseim-pgsql.toml @@ -38,16 +38,12 @@ 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" password = "makotaipsa" - [[listen.http.handlers.mongoose_api_client]] - host = "localhost" - path = "/api/contacts/{:jid}" - [[listen.http.handlers.mod_bosh]] host = "_" path = "/http-bind" @@ -65,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" @@ -82,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 @@ -180,12 +166,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" diff --git a/test/ejabberd_sm_SUITE.erl b/test/ejabberd_sm_SUITE.erl index fd09010654..aa85adcfa8 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), diff --git a/test/mongoose_api_common_SUITE.erl b/test/mongoose_api_common_SUITE.erl deleted file mode 100644 index eb47778091..0000000000 --- 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). - 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 d95d406894..992e10d492 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]"). diff --git a/test/mongoose_config_SUITE.erl b/test/mongoose_config_SUITE.erl index 65915c35d1..87d4895568 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, mongoose_server_api, join_cluster, [atom_to_list(node())]), case Result of {ok, _} -> diff --git a/test/mongoose_listener_SUITE_data/mongooseim.basic.toml b/test/mongoose_listener_SUITE_data/mongooseim.basic.toml index ccd65946f5..5e0533f8f0 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 diff --git a/tools/db-versions.sh b/tools/db-versions.sh index e66d6b9444..3b66b2a7c6 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 diff --git a/tools/test_runner/apply_templates.erl b/tools/test_runner/apply_templates.erl index 382a21be71..cef57a23d4 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).