diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 2b4eca581..c5c34b743 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -28,6 +28,23 @@ -type oauth_scope() :: atom(). +%% ejabberd_commands OAuth ReST ACL definition: +%% Two fields exist that are used to control access on a command from ReST API: +%% 1. Policy +%% If policy is: +%% - restricted: command is not exposed as OAuth Rest API. +%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access +%% - user: Command might be called by any server user. +%% - open: Command can be called by anyone. +%% +%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as +%% defined by access option. +%% Access option can be a list of: +%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command. +%% - AccessRule name: direct name of the access rule to check in config file. +%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter +%% to command, so that the command can perform additional check. + -record(ejabberd_commands, {name :: atom(), tags = [] :: [atom()] | '_' | '$2', @@ -38,19 +55,25 @@ function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, + %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}] + access = [] :: [{atom(),atom(),atom()}|atom()], result = {res, rescode} :: rterm() | '_' | '$2', args_desc = none :: none | [string()] | '_', result_desc = none :: none | string() | '_', args_example = none :: none | [any()] | '_', result_example = none :: any()}). +%% TODO Fix me: Type is not up to date -type ejabberd_commands() :: #ejabberd_commands{name :: atom(), tags :: [atom()], desc :: string(), longdesc :: string(), + version :: integer(), module :: atom(), function :: atom(), args :: [aterm()], + policy :: open | restricted | admin | user, + access :: [{atom(),atom(),atom()}|atom()], result :: rterm()}. %% @type ejabberd_commands() = #ejabberd_commands{ diff --git a/lib/ct_formatter.ex b/lib/ct_formatter.ex index 47c487ac4..0c301353b 100644 --- a/lib/ct_formatter.ex +++ b/lib/ct_formatter.ex @@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do use GenEvent - import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5, + import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5, format_test_case_failure: 5] def init(opts) do diff --git a/mix.exs b/mix.exs index 03ce64da2..7453ea473 100644 --- a/mix.exs +++ b/mix.exs @@ -11,6 +11,8 @@ defmodule Ejabberd.Mixfile do compilers: [:asn1] ++ Mix.compilers, erlc_options: erlc_options, erlc_paths: ["asn1", "src"], + # Elixir tests are starting the part of ejabberd they need + aliases: [test: "test --no-start"], package: package, deps: deps] end @@ -59,7 +61,9 @@ defmodule Ejabberd.Mixfile do {:exrm, "~> 1.0.0", only: :dev}, # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with # version 3.20: - {:relx, "~> 3.19.0", only: :dev}] + {:relx, "~> 3.19.0", only: :dev}, + {:meck, "~> 0.8.4", only: :test}, + {:moka, github: "processone/moka", tag: "1.0.5b", only: :test}] end defp package do diff --git a/mix.lock b/mix.lock index eda31f4a9..467f142ba 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,7 @@ "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, - "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:stun, "1.0.6", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}, + "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:stun, "1.0.6", [hex: :stun, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, "fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, @@ -14,13 +14,16 @@ "iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, + "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, + "moka": {:git, "https://github.com/processone/moka.git", "768efea96443c57125e6247dbebee687f17be149", [tag: "1.0.5b"]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []}, "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]}, + "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, + "samerlib": {:git, "https://github.com/processone/samerlib", "9158f65d18ec63f8b409543b6fb46dd5fce46160", [tag: "0.8.0b"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}} + "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}} diff --git a/src/acl.erl b/src/acl.erl index 07f7e3c12..897996976 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -31,7 +31,7 @@ -export([add_access/3, clear/0]). -export([start/0, add/3, add_list/3, add_local/3, add_list_local/3, - load_from_config/0, match_rule/3, + load_from_config/0, match_rule/3, any_rules_allowed/3, transform_options/1, opt_type/1, acl_rule_matches/3, acl_rule_verify/1, access_matches/3, transform_access_rules_config/1, @@ -275,6 +275,15 @@ normalize_spec(Spec) -> end end. +-spec any_rules_allowed(global | binary(), access_name(), + jid() | ljid() | inet:ip_address()) -> boolean(). + +any_rules_allowed(Host, Access, Entity) -> + lists:any(fun (Rule) -> + allow == acl:match_rule(Host, Rule, Entity) + end, + Access). + -spec match_rule(global | binary(), access_name(), jid() | ljid() | inet:ip_address()) -> any(). diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 87ac76875..f20aeebf0 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -129,6 +129,8 @@ get_commands_spec() -> #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", + policy = admin, + access = [{mod_register, access, configure}], module = ?MODULE, function = register, args = [{user, binary}, {host, binary}, {password, binary}], result = {res, restuple}}, @@ -166,7 +168,7 @@ get_commands_spec() -> #ejabberd_commands{name = list_cluster, tags = [cluster], desc = "List nodes that are part of the cluster handled by Node", module = ?MODULE, function = list_cluster, - args = [], + args = [], result = {nodes, {list, {node, atom}}}}, #ejabberd_commands{name = import_file, tags = [mnesia], @@ -220,7 +222,7 @@ get_commands_spec() -> desc = "Delete offline messages older than DAYS", module = ?MODULE, function = delete_old_messages, args = [{days, integer}], result = {res, rescode}}, - + #ejabberd_commands{name = export2sql, tags = [mnesia], desc = "Export virtual host information from Mnesia tables to SQL files", module = ejd2sql, function = export, diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 075ff35cf..33edcb7c7 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -223,9 +223,10 @@ get_command_definition/2, get_tags_commands/0, get_tags_commands/1, - get_commands/0, + get_exposed_commands/0, register_commands/1, - unregister_commands/1, + unregister_commands/1, + expose_commands/1, execute_command/2, execute_command/3, execute_command/4, @@ -275,10 +276,10 @@ get_commands_spec() -> init() -> mnesia:delete_table(ejabberd_commands), mnesia:create_table(ejabberd_commands, - [{ram_copies, [node()]}, + [{ram_copies, [node()]}, {local_content, true}, - {attributes, record_info(fields, ejabberd_commands)}, - {type, bag}]), + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), mnesia:add_table_copy(ejabberd_commands, node(), ram_copies), register_commands(get_commands_spec()). @@ -287,12 +288,14 @@ init() -> %% @doc Register ejabberd commands. %% If a command is already registered, a warning is printed and the %% old command is preserved. +%% A registered command is not directly available to be called through +%% ejabberd ReST API. It need to be exposed to be available through API. register_commands(Commands) -> lists:foreach( fun(Command) -> - % XXX check if command exists - mnesia:dirty_write(Command) - % ?DEBUG("This command is already defined:~n~p", [Command]) + %% XXX check if command exists + mnesia:dirty_write(Command) + %% ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands). @@ -306,6 +309,25 @@ unregister_commands(Commands) -> end, Commands). +%% @doc Expose command through ejabberd ReST API. +%% Pass a list of command names or policy to expose. +-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}. + +expose_commands(Commands) -> + Names = lists:map(fun(#ejabberd_commands{name = Name}) -> + Name; + (Name) when is_atom(Name) -> + Name + end, + Commands), + + case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of + {aborted, Reason} -> + {error, Reason}; + {atomic, Result} -> + Result + end. + -spec list_commands() -> [{atom(), [aterm()], string()}]. %% @doc Get a list of all the available commands, arguments and description. @@ -319,8 +341,8 @@ list_commands() -> list_commands(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc} || #ejabberd_commands{name = Name, - args = Args, - desc = Desc} <- Commands]. + args = Args, + desc = Desc} <- Commands]. -spec list_commands_policy(integer()) -> @@ -331,10 +353,10 @@ list_commands(Version) -> list_commands_policy(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc, Policy} || - #ejabberd_commands{name = Name, - args = Args, - desc = Desc, - policy = Policy} <- Commands]. + #ejabberd_commands{name = Name, + args = Args, + desc = Desc, + policy = Policy} <- Commands]. -spec get_command_format(atom()) -> {[aterm()], rterm()}. @@ -356,14 +378,14 @@ get_command_format(Name, Auth, Version) -> Admin = is_admin(Name, Auth, #{}), #ejabberd_commands{args = Args, result = Result, - policy = Policy} = - get_command_definition(Name, Version), + policy = Policy} = + get_command_definition(Name, Version), case Policy of - user when Admin; - Auth == noauth -> - {[{user, binary}, {server, binary} | Args], Result}; - _ -> - {Args, Result} + user when Admin; + Auth == noauth -> + {[{user, binary}, {server, binary} | Args], Result}; + _ -> + {Args, Result} end. -spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}. @@ -394,16 +416,16 @@ get_command_definition(Name) -> %% @doc Get the definition record of a command in a given API version. get_command_definition(Name, Version) -> case lists:reverse( - lists:sort( - mnesia:dirty_select( - ejabberd_commands, - ets:fun2ms( - fun(#ejabberd_commands{name = N, version = V} = C) - when N == Name, V =< Version -> - {V, C} - end)))) of - [{_, Command} | _ ] -> Command; - _E -> throw(unknown_command) + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = N, version = V} = C) + when N == Name, V =< Version -> + {V, C} + end)))) of + [{_, Command} | _ ] -> Command; + _E -> throw(unknown_command) end. -spec get_commands_definition(integer()) -> [ejabberd_commands()]. @@ -411,20 +433,20 @@ get_command_definition(Name, Version) -> % @doc Returns all commands for a given API version get_commands_definition(Version) -> L = lists:reverse( - lists:sort( - mnesia:dirty_select( - ejabberd_commands, - ets:fun2ms( - fun(#ejabberd_commands{name = Name, version = V} = C) - when V =< Version -> - {Name, V, C} - end)))), + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = Name, version = V} = C) + when V =< Version -> + {Name, V, C} + end)))), F = fun({_Name, _V, Command}, []) -> - [Command]; - ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> - Acc; - ({_Name, _V, Command}, Acc) -> [Command | Acc] - end, + [Command]; + ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + Acc; + ({_Name, _V, Command}, Acc) -> [Command | Acc] + end, lists:foldl(F, [], L). %% @spec (Name::atom(), Arguments) -> ResultTerm @@ -433,7 +455,7 @@ get_commands_definition(Version) -> %% @doc Execute a command. %% Can return the following exceptions: %% command_unknown | account_unprivileged | invalid_account_data | -%% no_auth_provided +%% no_auth_provided | access_rules_unauthorized execute_command(Name, Arguments) -> execute_command(Name, Arguments, ?DEFAULT_VERSION). @@ -494,41 +516,62 @@ execute_command(AccessCommands, Auth, Name, Arguments) -> %% %% @doc Execute a command in a given API version %% Can return the following exceptions: -%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> -execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). + execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> Auth = case is_admin(Name, Auth1, CallerInfo) of true -> admin; false -> Auth1 end, + TokenJID = oauth_token_user(Auth1), Command = get_command_definition(Name, Version), - AccessCommands = get_access_commands(AccessCommands1, Version), + AccessCommands = get_all_access_commands(AccessCommands1), + case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of - ok -> execute_command2(Auth, Command, Arguments) + ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) end. -execute_command2( - _Auth, #ejabberd_commands{policy = open} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - admin, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - noauth, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, [User, Server | Arguments]). -execute_command2(Command, Arguments) -> +execute_check_policy( + _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) -> + execute_check_access(JID, Command, Arguments); +execute_check_policy( + admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_check_access(JID, Command, Arguments); +execute_check_policy( + noauth, _JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_check_access(JID, Command, [User, Server | Arguments]). + +execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) -> + %% TODO Review: Do we have smarter / better way to check rule on other Host than global ? + Host = global, + Rules = lists:map(fun({Mod, AccessName, Default}) -> + gen_mod:get_module_opt(Host, Mod, + AccessName, fun(A) -> A end, Default); + (Default) -> + Default + end, AccessRefs), + case acl:any_rules_allowed(Host, Rules, FromJID) of + true -> + do_execute_command(Command, Arguments); + false -> + throw({error, access_rules_unauthorized}) + end. + +do_execute_command(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), @@ -598,31 +641,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI Command1 end, AccessCommandsAllowed = - lists:filter( - fun({Access, Commands, ArgumentRestrictions}) -> - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end; - ({Access, Commands}) -> - ArgumentRestrictions = [], - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end - end, - AccessCommands), + lists:filter( + fun({Access, Commands, ArgumentRestrictions}) -> + case check_access(Command, Access, Auth, CallerInfo) of + true -> + check_access_command(Commands, Command, + ArgumentRestrictions, + Method, Arguments); + false -> + false + end; + ({Access, Commands}) -> + ArgumentRestrictions = [], + case check_access(Command, Access, Auth, CallerInfo) of + true -> + check_access_command(Commands, Command, + ArgumentRestrictions, + Method, Arguments); + false -> + false + end + end, + AccessCommands), case AccessCommandsAllowed of - [] -> throw({error, account_unprivileged}); - L when is_list(L) -> ok + [] -> throw({error, account_unprivileged}); + L when is_list(L) -> ok end. -spec check_auth(ejabberd_commands(), noauth) -> noauth_provided; @@ -686,9 +729,9 @@ check_access2(Access, AccessInfo, Server) -> check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, - Arguments); - false -> false + true -> check_access_arguments(Command, ArgumentRestrictions, + Arguments); + false -> false end. check_access_arguments(Command, ArgumentRestrictions, Arguments) -> @@ -711,19 +754,23 @@ tag_arguments(ArgsDefs, Args) -> Args). +%% Get commands for all version +get_all_access_commands(AccessCommands) -> + get_access_commands(AccessCommands, ?DEFAULT_VERSION). + get_access_commands(undefined, Version) -> - Cmds = get_commands(Version), + Cmds = get_exposed_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; get_access_commands(AccessCommands, _Version) -> AccessCommands. -get_commands() -> - get_commands(?DEFAULT_VERSION). -get_commands(Version) -> +get_exposed_commands() -> + get_exposed_commands(?DEFAULT_VERSION). +get_exposed_commands(Version) -> Opts0 = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, - []), + []), Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0), CommandsList = list_commands_policy(Version), OpenCmds = [N || {N, _, _, open} <- CommandsList], @@ -733,27 +780,32 @@ get_commands(Version) -> Cmds = lists:foldl( fun([{add_commands, L}], Acc) -> - Cmds = case L of - open -> OpenCmds; - restricted -> RestrictedCmds; - admin -> AdminCmds; - user -> UserCmds; - _ when is_list(L) -> L - end, + Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds), lists:usort(Cmds ++ Acc); ([{remove_commands, L}], Acc) -> - Cmds = case L of - open -> OpenCmds; - restricted -> RestrictedCmds; - admin -> AdminCmds; - user -> UserCmds; - _ when is_list(L) -> L - end, + Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds), Acc -- Cmds; (_, Acc) -> Acc - end, AdminCmds ++ UserCmds, Opts), + end, [], Opts), Cmds. +%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry +expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) -> + lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc; + (user, Acc) -> UserCmds ++ Acc; + (admin, Acc) -> AdminCmds ++ Acc; + (restricted, Acc) -> RestrictedCmds ++ Acc; + (Command, Acc) when is_atom(Command) -> + [Command|Acc] + end, [], L). + +oauth_token_user(noauth) -> + undefined; +oauth_token_user(admin) -> + undefined; +oauth_token_user({User, Server, _, _}) -> + jid:make(User, Server, <<>>). + is_admin(_Name, admin, _Extra) -> true; is_admin(_Name, {_User, _Server, _, false}, _Extra) -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index d4b1ff87e..0af158562 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -66,7 +66,7 @@ %% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin %% (as it has access to ejabberd command line). --define(EXPIRE, 3600). +-define(EXPIRE, 31536000). start() -> DBMod = get_db_backend(), @@ -215,7 +215,7 @@ authenticate_user({User, Server}, Ctx) -> authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> - Cmds = ejabberd_commands:get_commands(), + Cmds = ejabberd_commands:get_exposed_commands(), Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds], RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1], case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), @@ -237,7 +237,7 @@ get_cmd_scopes() -> dict:append(Scope, Cmd, Accum2) end, Accum, Scopes); _ -> Accum - end end, dict:new(), ejabberd_commands:get_commands()), + end end, dict:new(), ejabberd_commands:get_exposed_commands()), ScopeMap. %% This is callback for oauth tokens generated through the command line. Only open and admin commands are @@ -371,12 +371,10 @@ process(_Handlers, ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]), ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], [ - ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>}, - {<<"value">>, jlib:integer_to_binary(expire())}],<<"Default (", (integer_to_binary(expire()))/binary, " seconds)">>), ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), - ?XAC(<<"option">>, [{<<"value">>, <<"31536000">>}],<<"1 Year">>), + ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>), ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), ?BR, ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) @@ -482,7 +480,7 @@ process(_Handlers, process(_Handlers, #request{method = 'POST', q = Q, lang = _Lang, path = [_, <<"token">>]}) -> - case proplists:get_value(<<"grant_type">>, Q, <<"">>) of + case proplists:get_value(<<"grant_type">>, Q, <<"">>) of <<"password">> -> SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), @@ -518,13 +516,10 @@ process(_Handlers, {<<"scope">>, str:join(VerifiedScope, <<" ">>)}, {<<"expires_in">>, Expires}]}); {error, Error} when is_atom(Error) -> - json_response(400, {[ - {<<"error">>, <<"invalid_grant">>}, - {<<"error_description">>, Error}]}) + json_error(400, <<"invalid_grant">>, Error) end; - _OtherGrantType -> - json_response(400, {[ - {<<"error">>, <<"unsupported_grant_type">>}]}) + _OtherGrantType -> + json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type) end; process(_Handlers, _Request) -> @@ -540,14 +535,24 @@ get_db_backend() -> list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)). -%% Headers as per RFC 6749 +%% Headers as per RFC 6749 json_response(Code, Body) -> {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, - {<<"Cache-Control">>, <<"no-store">>}, - {<<"Pragma">>, <<"no-cache">>}], + {<<"Cache-Control">>, <<"no-store">>}, + {<<"Pragma">>, <<"no-cache">>}], jiffy:encode(Body)}. +%% OAauth error are defined in: +%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2 +json_error(Code, Error, Reason) -> + Desc = json_error_desc(Reason), + Body = {[{<<"error">>, Error}, + {<<"error_description">>, Desc}]}, + json_response(Code, Body). +json_error_desc(access_denied) -> <<"Access denied">>; +json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>; +json_error_desc(invalid_scope) -> <<"Invalid scope">>. web_head() -> [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, @@ -661,7 +666,7 @@ css() -> text-decoration: underline; } - .container > .section { + .container > .section { background: #424A55; } diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 62f2eb7fa..6583fb445 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -96,12 +96,6 @@ get_acl_rule(_RPath, 'POST') -> access, fun(A) -> A end, configure), {global, [AC]}. -is_acl_match(Host, Rules, Jid) -> - lists:any(fun (Rule) -> - allow == acl:match_rule(Host, Rule, Jid) - end, - Rules). - %%%================================== %%%% Menu Items Access @@ -151,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) -> is_allowed_path(Path, JID); is_allowed_path(Path, JID) -> {HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'), - is_acl_match(HostOfRule, AccessRule, JID). + acl:any_rules_allowed(HostOfRule, AccessRule, JID). %% @spec(Path) -> URL %% where Path = [string()] @@ -279,8 +273,8 @@ get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> - case is_acl_match(HostOfRule, AccessRule, - jid:make(User, Server, <<"">>)) + case acl:any_rules_allowed(HostOfRule, AccessRule, + jid:make(User, Server, <<"">>)) of false -> {unauthorized, <<"unprivileged-account">>}; true -> {ok, {User, Server}} @@ -1346,7 +1340,7 @@ parse_access_rule(Text) -> list_vhosts(Lang, JID) -> Hosts = (?MYHOSTS), HostsAllowed = lists:filter(fun (Host) -> - is_acl_match(Host, + acl:any_rules_allowed(Host, [configure, webadmin_view], JID) end, diff --git a/src/jid.erl b/src/jid.erl index 0c3ac77c0..a730bd949 100644 --- a/src/jid.erl +++ b/src/jid.erl @@ -50,11 +50,35 @@ -spec start() -> ok. start() -> + {ok, Owner} = ets_owner(), SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]), - catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]), + %% Table is public to allow ETS insert to fix / update the table even if table already exist + %% with another owner. + catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]), ets:insert(jlib, {string_to_jid_pattern, SplitPattern}), ok. +ets_owner() -> + case whereis(jlib_ets) of + undefined -> + Pid = spawn(fun() -> ets_keepalive() end), + case catch register(jlib_ets, Pid) of + true -> + {ok, Pid}; + Error -> Error + end; + Pid -> + {ok,Pid} + end. + +%% Process used to keep jlib ETS table alive in case the original owner dies. +%% The table need to be public, otherwise subsequent inserts would fail. +ets_keepalive() -> + receive + _ -> + ets_keepalive() + end. + -spec make(binary(), binary(), binary()) -> jid() | error. make(User, Server, Resource) -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index b8aed94c2..ba3a14cf8 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -136,7 +136,7 @@ check_permissions(Request, Command) -> {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), check_permissions2(Request, Call, CommandPolicy, Scope); _ -> - unauthorized_response() + json_error(404, 40, <<"Endpoint not found.">>) end. check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList) @@ -220,8 +220,12 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = log(Call, Args, IPPort), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args, Version, IP), - json_response(Code, jiffy:encode(Result)); + case handle(Cmd, Auth, Args, Version, IP) of + {Code, Result} -> + json_response(Code, jiffy:encode(Result)); + {HTMLCode, JSONErrorCode, Message} -> + json_error(HTMLCode, JSONErrorCode, Message) + end; %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse @@ -264,10 +268,10 @@ get_api_version(#request{path = Path}) -> get_api_version(lists:reverse(Path)); get_api_version([<<"v", String/binary>> | Tail]) -> case catch jlib:binary_to_integer(String) of - N when is_integer(N) -> - N; - _ -> - get_api_version(Tail) + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail) end; get_api_version([_Head | Tail]) -> get_api_version(Tail); @@ -278,6 +282,8 @@ get_api_version([]) -> %% command handlers %% ---------------- +%% TODO Check accept types of request before decided format of reply. + % generic ejabberd command handler handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> case ejabberd_commands:get_command_format(Call, Auth, Version) of @@ -309,8 +315,10 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> {401, jlib:atom_to_binary(Why)}; throw:{not_allowed, Msg} -> {401, iolist_to_binary(Msg)}; - throw:{error, account_unprivileged} -> - {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)}; + throw:{error, account_unprivileged} -> + {403, 31, <<"Command need to be run with admin priviledge.">>}; + throw:{error, access_rules_unauthorized} -> + {403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>}; throw:{invalid_parameter, Msg} -> {400, iolist_to_binary(Msg)}; throw:{error, Why} when is_atom(Why) -> @@ -490,9 +498,7 @@ format_result(404, {_Name, _}) -> "not_found". unauthorized_response() -> - unauthorized_response(<<"401 Unauthorized">>). -unauthorized_response(Body) -> - json_response(401, jiffy:encode(Body)). + json_error(401, 10, <<"Oauth Token is invalid or expired.">>). badrequest_response() -> badrequest_response(<<"400 Bad Request">>). @@ -502,6 +508,15 @@ badrequest_response(Body) -> json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. +%% HTTPCode, JSONCode = integers +%% message is binary +json_error(HTTPCode, JSONCode, Message) -> + {HTTPCode, ?HEADER(?CT_JSON), + jiffy:encode({[{<<"status">>, <<"error">>}, + {<<"code">>, JSONCode}, + {<<"message">>, Message}]}) + }. + log(Call, Args, {Addr, Port}) -> AddrS = jlib:ip_to_list({Addr, Port}), ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]); diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 487cf6a4b..439a3c1d3 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -18,9 +18,13 @@ # # ---------------------------------------------------------------------- +## TODO Fix next test error: add admin user ACL + defmodule EjabberdCommandsMockTest do use ExUnit.Case, async: false + require EjabberdOauthMock + @author "jsautret@process-one.net" # mocked callback module @@ -44,8 +48,11 @@ defmodule EjabberdCommandsMockTest do _ -> :ok end :mnesia.start + :ok = :jid.start + :ok = :ejabberd_config.start(["domain1", "domain2"], []) + :ok = :acl.start EjabberdOauthMock.init - :ok + on_exit fn -> :meck.unload end end setup do @@ -180,7 +187,7 @@ defmodule EjabberdCommandsMockTest do test "API command with user policy" do - mock_commands_config + mock_commands_config [:user, :admin] # Register a command test(user, domain) -> {:versionN, user, domain} # with policy=user and versions 1 & 3 @@ -313,9 +320,8 @@ defmodule EjabberdCommandsMockTest do end - test "API command with admin policy" do - mock_commands_config + mock_commands_config [:admin] # Register a command test(user, domain) -> {user, domain} # with policy=admin @@ -393,13 +399,47 @@ defmodule EjabberdCommandsMockTest do assert :meck.validate @module end + test "Commands can perform extra check on access" do + mock_commands_config [:admin, :open] + + command_name = :test + function = :test_command + command = ejabberd_commands(name: command_name, + args: [{:user, :binary}, {:host, :binary}], + access: [:basic_rule_1], + module: @module, + function: function, + policy: :open) + :meck.expect(@module, function, + fn(user, domain) when is_binary(user) and is_binary(domain) -> + {user, domain} + end) + assert :ok == :ejabberd_commands.register_commands [command] + +# :acl.add(:global, :basic_acl_1, {:user, @user, @host}) +# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) + + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [@user, @domain]) + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, false}, + command_name, + [@user, @domain]) + + end ########################################################## # Utils # Mock a config where only @admin user is allowed to call commands # as admin - def mock_commands_config do + def mock_commands_config(commands \\ []) do EjabberdAuthMock.init EjabberdAuthMock.create_user @user, @domain, @userpass EjabberdAuthMock.create_user @admin, @domain, @adminpass @@ -408,10 +448,12 @@ defmodule EjabberdCommandsMockTest do :meck.expect(:ejabberd_config, :get_option, fn(:commands_admin_access, _, _) -> :commands_admin_access (:oauth_access, _, _) -> :all + (:commands, _, _) -> [{:add_commands, commands}] (_, _, default) -> default end) :meck.expect(:ejabberd_config, :get_myhosts, fn() -> [@domain] end) + :meck.new :acl :meck.expect(:acl, :access_matches, fn(:commands_admin_access, info, _scope) -> diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs index 31d108214..10b656140 100644 --- a/test/ejabberd_commands_test.exs +++ b/test/ejabberd_commands_test.exs @@ -28,7 +28,11 @@ defmodule EjabberdCommandsTest do setup_all do :mnesia.start + :stringprep.start + :ok = :ejabberd_config.start(["localhost"], []) + :ejabberd_commands.init + :ok end test "Check that we can register a command" do @@ -37,6 +41,14 @@ defmodule EjabberdCommandsTest do assert Enum.member?(commands, {:test_user, [], "Test user"}) end + test "get_exposed_commands/0 returns registered commands" do + commands = [open_test_command] + :ok = :ejabberd_commands.register_commands(commands) + :ok = :ejabberd_commands.expose_commands(commands) + exposed_commands = :ejabberd_commands.get_exposed_commands + assert Enum.member?(exposed_commands, :test_open) + end + test "Check that admin commands are rejected with noauth credentials" do :ok = :ejabberd_commands.register_commands([admin_test_command]) @@ -70,6 +82,16 @@ defmodule EjabberdCommandsTest do ]}}}}) end + defp open_test_command do + ejabberd_commands(name: :test_open, tags: [:test], + desc: "Test open", + policy: :open, + module: __MODULE__, + function: :test_open, + args: [], + result: {:res, :rescode}) + end + defp admin_test_command do ejabberd_commands(name: :test_admin, tags: [:roster], desc: "Test admin", diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs index 0dc64ee44..d9b949294 100644 --- a/test/ejabberd_cyrsasl_test.exs +++ b/test/ejabberd_cyrsasl_test.exs @@ -71,8 +71,8 @@ defmodule EjabberdCyrsaslTest do response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <> "nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <> "charset=utf-8,algorithm=md5-sess" - assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response) - assert {:ok, list} = :cyrsasl.server_step(state3, "") + assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response) + assert {:ok, _list} = :cyrsasl.server_step(state3, "") end defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do @@ -94,7 +94,7 @@ defmodule EjabberdCyrsaslTest do defp setup_anonymous_mocks() do :meck.unload mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled, - fn (host) -> + fn (_host) -> true end) mock(:ejabberd_auth, :is_user_exists, @@ -119,7 +119,7 @@ defmodule EjabberdCyrsaslTest do end end - defp check_password(user, authzid, pass) do + defp check_password(_user, authzid, pass) do case get_password(authzid) do {^pass, mod} -> {true, mod} @@ -128,7 +128,7 @@ defmodule EjabberdCyrsaslTest do end end - defp check_password_digest(user, authzid, pass, digest, digest_gen) do + defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do case get_password(authzid) do {spass, mod} -> v = digest_gen.(spass) diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs index 761b07b7c..03422264f 100644 --- a/test/mod_admin_extra_test.exs +++ b/test/mod_admin_extra_test.exs @@ -22,6 +22,9 @@ defmodule EjabberdModAdminExtraTest do use ExUnit.Case, async: false require EjabberdAuthMock + require EjabberdSmMock + require ModLastMock + require ModRosterMock @author "jsautret@process-one.net" diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index fcfdfee13..9cba35365 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -73,7 +73,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) -> @@ -126,7 +126,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, {:oauth, _token}, false}, @@ -219,7 +219,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, {:oauth, _token}, false}, diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index 99b8d9b28..e2ae3d784 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -31,43 +31,43 @@ defmodule ModHttpApiTest do :ok = :mnesia.start :stringprep.start :ok = :ejabberd_config.start(["localhost"], []) - :ok = :ejabberd_commands.init - :ok = :ejabberd_commands.register_commands(cmds) - on_exit fn -> unregister_commands(cmds) end + on_exit fn -> + :meck.unload + unregister_commands(cmds) end end test "We can expose several commands to API at a time" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]]) - commands = :ejabberd_commands.get_commands() + :ejabberd_commands.expose_commands([:open_cmd, :user_cmd]) + commands = :ejabberd_commands.get_exposed_commands() assert Enum.member?(commands, :open_cmd) assert Enum.member?(commands, :user_cmd) end test "We can call open commands without authentication" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]]) + :ejabberd_commands.expose_commands([:open_cmd]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {200, _, _} = :mod_http_api.process(["open_cmd"], request) end # This related to the commands config file option - test "Attempting to access a command that is not exposed as HTTP API returns 401" do + test "Attempting to access a command that is not exposed as HTTP API returns 403" do setup_mocks() - :ejabberd_config.add_local_option(:commands, []) + :ejabberd_commands.expose_commands([]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {401, _, _} = :mod_http_api.process(["open_cmd"], request) + {403, _, _} = :mod_http_api.process(["open_cmd"], request) end test "Call to user, admin or restricted commands without authentication are rejected" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]]) + :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {401, _, _} = :mod_http_api.process(["user_cmd"], request) - {401, _, _} = :mod_http_api.process(["admin_cmd"], request) - {401, _, _} = :mod_http_api.process(["restricted_cmd"], request) + {403, _, _} = :mod_http_api.process(["user_cmd"], request) + {403, _, _} = :mod_http_api.process(["admin_cmd"], request) + {403, _, _} = :mod_http_api.process(["restricted_cmd"], request) end @tag pending: true @@ -98,7 +98,7 @@ defmodule ModHttpApiTest do defp setup_mocks() do :meck.unload mock(:gen_mod, :get_module_opt, - fn (_server, :mod_http_api, admin_ip_access, _, _) -> + fn (_server, :mod_http_api, _admin_ip_access, _, _) -> [{:allow, [{:ip, {{127,0,0,2}, 32}}]}] end) end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 000000000..454f2338a --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,7 @@ +Code.require_file "ejabberd_auth_mock.exs", __DIR__ +Code.require_file "ejabberd_oauth_mock.exs", __DIR__ +Code.require_file "ejabberd_sm_mock.exs", __DIR__ +Code.require_file "mod_last_mock.exs", __DIR__ +Code.require_file "mod_roster_mock.exs", __DIR__ + +ExUnit.start