diff --git a/test/ejabberd_commands_auth_test.exs b/test/ejabberd_commands_auth_test.exs deleted file mode 100644 index f415fee51..000000000 --- a/test/ejabberd_commands_auth_test.exs +++ /dev/null @@ -1,81 +0,0 @@ -# ---------------------------------------------------------------------- -# -# ejabberd, Copyright (C) 2002-2016 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. -# -# ---------------------------------------------------------------------- - -defmodule EjabberdCommandsTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :ejabberd_commands.init - end - - test "Check that we can register a command" do - :ok = :ejabberd_commands.register_commands([user_test_command]) - commands = :ejabberd_commands.list_commands - assert Enum.member?(commands, {:test_user, [], "Test user"}) - end - - test "Check that admin commands are rejected with noauth credentials" do - :ok = :ejabberd_commands.register_commands([admin_test_command]) - {:error, :account_unprivileged} = :ejabberd_commands.execute_command(:undefined, :noauth, :test_admin, []) - # Command executed from ejabberdctl passes anyway with access commands trick - # TODO: We should refactor to have explicit call when bypassing auth check for command-line - :ok = :ejabberd_commands.execute_command([], :noauth, :test_admin, []) - end - - # TODO Test that we can add command to list of expose commands - # This can be done with: - # ejabberd_config:add_local_option(commands, [[{add_commands, [open_cmd]}]]). - -# test "Check that a user can use a user command" do -# [Command] = ets:lookup(ejabberd_commands, test_user), -# AccessCommands = ejabberd_commands:get_access_commands(undefined), -# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []). -# end - - defp user_test_command do - ejabberd_commands(name: :test_user, tags: [:roster], - desc: "Test user", - policy: :user, - module: __MODULE__, - function: :test_user, - args: [], - result: {:contacts, {:list, {:contact, {:tuple, [ - {:jid, :string}, - {:nick, :string} - ]}}}}) - end - - defp admin_test_command do - ejabberd_commands(name: :test_admin, tags: [:roster], - desc: "Test admin", - policy: :restricted, - module: __MODULE__, - function: :test_admin, - args: [], - result: {:res, :rescode}) - end - - def test_admin, do: :ok -end diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs new file mode 100644 index 000000000..b3f10000e --- /dev/null +++ b/test/ejabberd_commands_mock_test.exs @@ -0,0 +1,424 @@ +# ---------------------------------------------------------------------- +# +# ejabberd, Copyright (C) 2002-2016 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. +# +# ---------------------------------------------------------------------- + +defmodule EjabberdCommandsTest do + use ExUnit.Case, async: false + + @author "jsautret@process-one.net" + + # mocked callback module + @module :test_module + # Admin user + @admin "admin" + @adminpass "adminpass" + # Non admin user + @user "user" + @userpass "userpass" + # XMPP domain + @domain "domain" + + require Record + Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, + from: "ejabberd_commands.hrl") + + setup_all do + try do + :stringprep.start + rescue + _ -> :ok + end + :mnesia.start + EjabberdOauthMock.init + :ok + end + + setup do + :meck.unload + :meck.new(@module, [:non_strict]) + :ejabberd_commands.init + end + + test "API command can be registered, listed and unregistered" do + command = ejabberd_commands name: :test, module: @module, + function: :test_command + + assert :ok == :ejabberd_commands.register_commands [command] + commands = :ejabberd_commands.list_commands + assert Enum.member? commands, {:test, [], ''} + + assert :ok == :ejabberd_commands.unregister_commands [command] + commands = :ejabberd_commands.list_commands + refute Enum.member? commands, {:test, [], ''} + end + + + test "API command with versions can be registered, listed and unregistered" do + command1 = ejabberd_commands name: :test, module: @module, + function: :test_command, version: 1, desc: 'version1' + command3 = ejabberd_commands name: :test, module: @module, + function: :test_command, version: 3, desc: 'version3' + assert :ejabberd_commands.register_commands [command1, command3] + + version1 = {:test, [], 'version1'} + version3 = {:test, [], 'version3'} + + # default version is latest one + commands = :ejabberd_commands.list_commands + refute Enum.member? commands, version1 + assert Enum.member? commands, version3 + + # no such command in APIv0 + commands = :ejabberd_commands.list_commands 0 + refute Enum.member? commands, version1 + refute Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 1 + assert Enum.member? commands, version1 + refute Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 2 + assert Enum.member? commands, version1 + refute Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 3 + refute Enum.member? commands, version1 + assert Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 4 + refute Enum.member? commands, version1 + assert Enum.member? commands, version3 + + assert :ok == :ejabberd_commands.unregister_commands [command1] + + commands = :ejabberd_commands.list_commands 1 + refute Enum.member? commands, version1 + refute Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 3 + refute Enum.member? commands, version1 + assert Enum.member? commands, version3 + + assert :ok == :ejabberd_commands.unregister_commands [command3] + + commands = :ejabberd_commands.list_commands 1 + refute Enum.member? commands, version1 + refute Enum.member? commands, version3 + + commands = :ejabberd_commands.list_commands 3 + refute Enum.member? commands, version1 + refute Enum.member? commands, version3 + end + + + test "API command can be registered and executed" do + # Create & register a mocked command test() -> :result + command_name = :test + function = :test_command + command = ejabberd_commands(name: command_name, + module: @module, + function: function) + :meck.expect @module, function, fn -> :result end + assert :ok == :ejabberd_commands.register_commands [command] + + assert :result == :ejabberd_commands.execute_command(command_name, []) + + assert :meck.validate @module + end + + test "API command with versions can be registered and executed" do + command_name = :test + + function1 = :test_command1 + command1 = ejabberd_commands(name: command_name, + version: 1, + module: @module, + function: function1) + :meck.expect(@module, function1, fn -> :result1 end) + + function3 = :test_command3 + command3 = ejabberd_commands(name: command_name, + version: 3, + module: @module, + function: function3) + :meck.expect(@module, function3, fn -> :result3 end) + + assert :ok == :ejabberd_commands.register_commands [command1, command3] + + # default version is latest one + assert :result3 == :ejabberd_commands.execute_command(command_name, []) + # no such command in APIv0 + assert :unknown_command == + catch_throw :ejabberd_commands.execute_command(command_name, [], 0) + assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1) + assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2) + assert :result3 == :ejabberd_commands.execute_command(command_name, [], 3) + assert :result3 == :ejabberd_commands.execute_command(command_name, [], 4) + + assert :meck.validate @module + end + + + + test "API command with user policy" do + mock_commands_config + + # Register a command test(user, domain) -> {:versionN, user, domain} + # with policy=user and versions 1 & 3 + command_name = :test + command1 = ejabberd_commands(name: command_name, + module: @module, + function: :test_command1, + policy: :user, version: 1) + command3 = ejabberd_commands(name: command_name, + module: @module, + function: :test_command3, + policy: :user, version: 3) + :meck.expect(@module, :test_command1, + fn(user, domain) when is_binary(user) and is_binary(domain) -> + {:version1, user, domain} + end) + :meck.expect(@module, :test_command3, + fn(user, domain) when is_binary(user) and is_binary(domain) -> + {:version3, user, domain} + end) + assert :ok == :ejabberd_commands.register_commands [command1, command3] + + # A normal user must not pass user info as parameter + assert {:version1, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [], 2) + assert {:version3, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [], 3) + token = EjabberdOauthMock.get_token @user, @domain, command_name + assert {:version3, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + {:oauth, token}, false}, + command_name, + [], 4) + # Expired oauth token + token = EjabberdOauthMock.get_token @user, @domain, command_name, 1 + :timer.sleep 1500 + assert {:error, :invalid_account_data} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + {:oauth, token}, false}, + command_name, + [], 4) + # Wrong oauth scope + token = EjabberdOauthMock.get_token @user, @domain, :bad_command + assert {:error, :invalid_account_data} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + {:oauth, token}, false}, + command_name, + [], 4) + + + assert :function_clause == + catch_error :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [@user, @domain], 2) + # @user is not admin + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, true}, + command_name, + [], 2) + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, true}, + command_name, + [@user, @domain], 2) + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + {:oauth, token}, true}, + command_name, + [@user, @domain], 2) + + + # An admin must explicitely pass user info + assert {:version1, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, :admin, + command_name, [@user, @domain], 2) + assert {:version3, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, :admin, + command_name, [@user, @domain], 4) + assert {:version1, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, @adminpass, true}, + command_name, [@user, @domain], 1) + token = EjabberdOauthMock.get_token @admin, @domain, command_name + assert {:version3, @user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, {:oauth, token}, true}, + command_name, [@user, @domain], 3) + # Wrong @admin password + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass<>"bad", true}, + command_name, + [@user, @domain], 3) + # @admin calling as a normal user + assert {:version3, @admin, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, false}, + command_name, [], 5) + assert {:version3, @admin, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + {:oauth, token}, false}, + command_name, [], 6) + assert :function_clause == + catch_error :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, false}, + command_name, + [@user, @domain], 5) + assert :meck.validate @module + end + + + + test "API command with admin policy" do + mock_commands_config + + # Register a command test(user, domain) -> {user, domain} + # with policy=admin + command_name = :test + function = :test_command + command = ejabberd_commands(name: command_name, + args: [{:user, :binary}, {:host, :binary}], + module: @module, + function: function, + policy: :admin) + :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] + + # A normal user cannot call the command + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [@user, @domain]) + + # An admin can call the command + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, true}, + command_name, + [@user, @domain]) + + # An admin can call the command with oauth token + token = EjabberdOauthMock.get_token @admin, @domain, command_name + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + {:oauth, token}, true}, + command_name, + [@user, @domain]) + + + # An admin with bad password cannot call the command + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + "bad"<>@adminpass, false}, + command_name, + [@user, @domain]) + + # An admin cannot call the command with bad oauth token + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + {:oauth, "bad"<>token}, true}, + command_name, + [@user, @domain]) + + # An admin as a normal user cannot call the command + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, false}, + command_name, + [@user, @domain]) + + # An admin as a normal user cannot call the command with oauth token + assert {:error, :account_unprivileged} == + catch_throw :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + {:oauth, token}, false}, + command_name, + [@user, @domain]) + + assert :meck.validate @module + end + + + ########################################################## + # Utils + + # Mock a config where only @admin user is allowed to call commands + # as admin + def mock_commands_config do + EjabberdAuthMock.init + EjabberdAuthMock.create_user @user, @domain, @userpass + EjabberdAuthMock.create_user @admin, @domain, @adminpass + + :meck.new :ejabberd_config + :meck.expect(:ejabberd_config, :get_option, + fn(:commands_admin_access, _, _) -> :commands_admin_access + (:oauth_access, _, _) -> :all + (_, _, default) -> default + end) + :meck.expect(:ejabberd_config, :get_myhosts, + fn() -> [@domain] end) + :meck.new :acl + :meck.expect(:acl, :match_rule, + fn(@domain, :commands_admin_access, user) -> + case :jlib.make_jid(@admin, @domain, "") do + ^user -> :allow + _ -> :deny + end + (@domain, :all, _user) -> + :allow + end) + end + +end diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs index b3f10000e..f415fee51 100644 --- a/test/ejabberd_commands_test.exs +++ b/test/ejabberd_commands_test.exs @@ -19,406 +19,63 @@ # ---------------------------------------------------------------------- defmodule EjabberdCommandsTest do - use ExUnit.Case, async: false + @author "mremond@process-one.net" - @author "jsautret@process-one.net" + use ExUnit.Case, async: true - # mocked callback module - @module :test_module - # Admin user - @admin "admin" - @adminpass "adminpass" - # Non admin user - @user "user" - @userpass "userpass" - # XMPP domain - @domain "domain" + require Record + Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, - from: "ejabberd_commands.hrl") + setup_all do + :ejabberd_commands.init + end - setup_all do - try do - :stringprep.start - rescue - _ -> :ok - end - :mnesia.start - EjabberdOauthMock.init - :ok - end + test "Check that we can register a command" do + :ok = :ejabberd_commands.register_commands([user_test_command]) + commands = :ejabberd_commands.list_commands + assert Enum.member?(commands, {:test_user, [], "Test user"}) + end - setup do - :meck.unload - :meck.new(@module, [:non_strict]) - :ejabberd_commands.init - end + test "Check that admin commands are rejected with noauth credentials" do + :ok = :ejabberd_commands.register_commands([admin_test_command]) + {:error, :account_unprivileged} = :ejabberd_commands.execute_command(:undefined, :noauth, :test_admin, []) + # Command executed from ejabberdctl passes anyway with access commands trick + # TODO: We should refactor to have explicit call when bypassing auth check for command-line + :ok = :ejabberd_commands.execute_command([], :noauth, :test_admin, []) + end - test "API command can be registered, listed and unregistered" do - command = ejabberd_commands name: :test, module: @module, - function: :test_command + # TODO Test that we can add command to list of expose commands + # This can be done with: + # ejabberd_config:add_local_option(commands, [[{add_commands, [open_cmd]}]]). - assert :ok == :ejabberd_commands.register_commands [command] - commands = :ejabberd_commands.list_commands - assert Enum.member? commands, {:test, [], ''} +# test "Check that a user can use a user command" do +# [Command] = ets:lookup(ejabberd_commands, test_user), +# AccessCommands = ejabberd_commands:get_access_commands(undefined), +# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []). +# end - assert :ok == :ejabberd_commands.unregister_commands [command] - commands = :ejabberd_commands.list_commands - refute Enum.member? commands, {:test, [], ''} - end + defp user_test_command do + ejabberd_commands(name: :test_user, tags: [:roster], + desc: "Test user", + policy: :user, + module: __MODULE__, + function: :test_user, + args: [], + result: {:contacts, {:list, {:contact, {:tuple, [ + {:jid, :string}, + {:nick, :string} + ]}}}}) + end + defp admin_test_command do + ejabberd_commands(name: :test_admin, tags: [:roster], + desc: "Test admin", + policy: :restricted, + module: __MODULE__, + function: :test_admin, + args: [], + result: {:res, :rescode}) + end - test "API command with versions can be registered, listed and unregistered" do - command1 = ejabberd_commands name: :test, module: @module, - function: :test_command, version: 1, desc: 'version1' - command3 = ejabberd_commands name: :test, module: @module, - function: :test_command, version: 3, desc: 'version3' - assert :ejabberd_commands.register_commands [command1, command3] - - version1 = {:test, [], 'version1'} - version3 = {:test, [], 'version3'} - - # default version is latest one - commands = :ejabberd_commands.list_commands - refute Enum.member? commands, version1 - assert Enum.member? commands, version3 - - # no such command in APIv0 - commands = :ejabberd_commands.list_commands 0 - refute Enum.member? commands, version1 - refute Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 1 - assert Enum.member? commands, version1 - refute Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 2 - assert Enum.member? commands, version1 - refute Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 3 - refute Enum.member? commands, version1 - assert Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 4 - refute Enum.member? commands, version1 - assert Enum.member? commands, version3 - - assert :ok == :ejabberd_commands.unregister_commands [command1] - - commands = :ejabberd_commands.list_commands 1 - refute Enum.member? commands, version1 - refute Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 3 - refute Enum.member? commands, version1 - assert Enum.member? commands, version3 - - assert :ok == :ejabberd_commands.unregister_commands [command3] - - commands = :ejabberd_commands.list_commands 1 - refute Enum.member? commands, version1 - refute Enum.member? commands, version3 - - commands = :ejabberd_commands.list_commands 3 - refute Enum.member? commands, version1 - refute Enum.member? commands, version3 - end - - - test "API command can be registered and executed" do - # Create & register a mocked command test() -> :result - command_name = :test - function = :test_command - command = ejabberd_commands(name: command_name, - module: @module, - function: function) - :meck.expect @module, function, fn -> :result end - assert :ok == :ejabberd_commands.register_commands [command] - - assert :result == :ejabberd_commands.execute_command(command_name, []) - - assert :meck.validate @module - end - - test "API command with versions can be registered and executed" do - command_name = :test - - function1 = :test_command1 - command1 = ejabberd_commands(name: command_name, - version: 1, - module: @module, - function: function1) - :meck.expect(@module, function1, fn -> :result1 end) - - function3 = :test_command3 - command3 = ejabberd_commands(name: command_name, - version: 3, - module: @module, - function: function3) - :meck.expect(@module, function3, fn -> :result3 end) - - assert :ok == :ejabberd_commands.register_commands [command1, command3] - - # default version is latest one - assert :result3 == :ejabberd_commands.execute_command(command_name, []) - # no such command in APIv0 - assert :unknown_command == - catch_throw :ejabberd_commands.execute_command(command_name, [], 0) - assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1) - assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2) - assert :result3 == :ejabberd_commands.execute_command(command_name, [], 3) - assert :result3 == :ejabberd_commands.execute_command(command_name, [], 4) - - assert :meck.validate @module - end - - - - test "API command with user policy" do - mock_commands_config - - # Register a command test(user, domain) -> {:versionN, user, domain} - # with policy=user and versions 1 & 3 - command_name = :test - command1 = ejabberd_commands(name: command_name, - module: @module, - function: :test_command1, - policy: :user, version: 1) - command3 = ejabberd_commands(name: command_name, - module: @module, - function: :test_command3, - policy: :user, version: 3) - :meck.expect(@module, :test_command1, - fn(user, domain) when is_binary(user) and is_binary(domain) -> - {:version1, user, domain} - end) - :meck.expect(@module, :test_command3, - fn(user, domain) when is_binary(user) and is_binary(domain) -> - {:version3, user, domain} - end) - assert :ok == :ejabberd_commands.register_commands [command1, command3] - - # A normal user must not pass user info as parameter - assert {:version1, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [], 2) - assert {:version3, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [], 3) - token = EjabberdOauthMock.get_token @user, @domain, command_name - assert {:version3, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - {:oauth, token}, false}, - command_name, - [], 4) - # Expired oauth token - token = EjabberdOauthMock.get_token @user, @domain, command_name, 1 - :timer.sleep 1500 - assert {:error, :invalid_account_data} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - {:oauth, token}, false}, - command_name, - [], 4) - # Wrong oauth scope - token = EjabberdOauthMock.get_token @user, @domain, :bad_command - assert {:error, :invalid_account_data} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - {:oauth, token}, false}, - command_name, - [], 4) - - - assert :function_clause == - catch_error :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [@user, @domain], 2) - # @user is not admin - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, true}, - command_name, - [], 2) - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, true}, - command_name, - [@user, @domain], 2) - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - {:oauth, token}, true}, - command_name, - [@user, @domain], 2) - - - # An admin must explicitely pass user info - assert {:version1, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, :admin, - command_name, [@user, @domain], 2) - assert {:version3, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, :admin, - command_name, [@user, @domain], 4) - assert {:version1, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, @adminpass, true}, - command_name, [@user, @domain], 1) - token = EjabberdOauthMock.get_token @admin, @domain, command_name - assert {:version3, @user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, {:oauth, token}, true}, - command_name, [@user, @domain], 3) - # Wrong @admin password - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass<>"bad", true}, - command_name, - [@user, @domain], 3) - # @admin calling as a normal user - assert {:version3, @admin, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, false}, - command_name, [], 5) - assert {:version3, @admin, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - {:oauth, token}, false}, - command_name, [], 6) - assert :function_clause == - catch_error :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, false}, - command_name, - [@user, @domain], 5) - assert :meck.validate @module - end - - - - test "API command with admin policy" do - mock_commands_config - - # Register a command test(user, domain) -> {user, domain} - # with policy=admin - command_name = :test - function = :test_command - command = ejabberd_commands(name: command_name, - args: [{:user, :binary}, {:host, :binary}], - module: @module, - function: function, - policy: :admin) - :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] - - # A normal user cannot call the command - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [@user, @domain]) - - # An admin can call the command - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, true}, - command_name, - [@user, @domain]) - - # An admin can call the command with oauth token - token = EjabberdOauthMock.get_token @admin, @domain, command_name - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - {:oauth, token}, true}, - command_name, - [@user, @domain]) - - - # An admin with bad password cannot call the command - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - "bad"<>@adminpass, false}, - command_name, - [@user, @domain]) - - # An admin cannot call the command with bad oauth token - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - {:oauth, "bad"<>token}, true}, - command_name, - [@user, @domain]) - - # An admin as a normal user cannot call the command - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, false}, - command_name, - [@user, @domain]) - - # An admin as a normal user cannot call the command with oauth token - assert {:error, :account_unprivileged} == - catch_throw :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - {:oauth, token}, false}, - command_name, - [@user, @domain]) - - assert :meck.validate @module - end - - - ########################################################## - # Utils - - # Mock a config where only @admin user is allowed to call commands - # as admin - def mock_commands_config do - EjabberdAuthMock.init - EjabberdAuthMock.create_user @user, @domain, @userpass - EjabberdAuthMock.create_user @admin, @domain, @adminpass - - :meck.new :ejabberd_config - :meck.expect(:ejabberd_config, :get_option, - fn(:commands_admin_access, _, _) -> :commands_admin_access - (:oauth_access, _, _) -> :all - (_, _, default) -> default - end) - :meck.expect(:ejabberd_config, :get_myhosts, - fn() -> [@domain] end) - :meck.new :acl - :meck.expect(:acl, :match_rule, - fn(@domain, :commands_admin_access, user) -> - case :jlib.make_jid(@admin, @domain, "") do - ^user -> :allow - _ -> :deny - end - (@domain, :all, _user) -> - :allow - end) - end - + def test_admin, do: :ok end diff --git a/test/http_api_test.exs b/test/http_api_test.exs deleted file mode 100644 index 2ec33bcbb..000000000 --- a/test/http_api_test.exs +++ /dev/null @@ -1,96 +0,0 @@ -# ---------------------------------------------------------------------- -# -# ejabberd, Copyright (C) 2002-2016 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. -# -# ---------------------------------------------------------------------- - -defmodule ModHttpApiTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :request, Record.extract(:request, from_lib: "ejabberd/include/ejabberd_http.hrl") - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :ok = :mnesia.start - :ok = :ejabberd_config.start(["localhost"], []) - - :ok = :ejabberd_commands.init - - :ok = :ejabberd_commands.register_commands(cmds) - on_exit fn -> unregister_commands(cmds) end - end - - test "We can expose several commands to API at a time" do - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]]) - commands = :ejabberd_commands.get_commands() - assert Enum.member?(commands, :open_cmd) - assert Enum.member?(commands, :user_cmd) - end - - test "We can call open commands without authentication" do - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]]) - request = request(method: :POST, 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 - :ejabberd_config.add_local_option(:commands, []) - request = request(method: :POST, data: "[]") - {401, _, _} = :mod_http_api.process(["open_cmd"], request) - end - - test "Call to user, admin or restricted commands without authentication are rejected" do - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]]) - request = request(method: :POST, 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) - end - - # Define a set of test commands that we expose through API - # We define one for each policy type - defp cmds do - [:open, :user, :admin, :restricted] - |> Enum.map(&({&1, String.to_atom(to_string(&1) <> "_cmd")})) - |> Enum.map(fn({cmd_type, cmd}) -> - ejabberd_commands(name: cmd, tags: [:test], - policy: cmd_type, - module: __MODULE__, - function: cmd, - args: [], - result: {:res, :rescode}) - end) - end - - def open_cmd, do: :ok - def user_cmd, do: :ok - def admin_cmd, do: :ok - def restricted_cmd, do: :ok - - defp unregister_commands(commands) do - try do - :ejabberd_commands.unregister_commands(commands) - catch - _,_ -> :ok - end - end - -end diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs new file mode 100644 index 000000000..ae62f28f9 --- /dev/null +++ b/test/mod_http_api_mock_test.exs @@ -0,0 +1,188 @@ +# ---------------------------------------------------------------------- +# +# ejabberd, Copyright (C) 2002-2015 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. +# +# ---------------------------------------------------------------------- + +defmodule ModHttpApiTest do + use ExUnit.Case, async: false + + @author "jsautret@process-one.net" + + # Admin user + @admin "admin" + @adminpass "adminpass" + # Non admin user + @user "user" + @userpass "userpass" + # XMPP domain + @domain "domain" + # mocked command + @command "command_test" + @acommand String.to_atom(@command) + # default API version + @version 0 + + require Record + Record.defrecord :request, Record.extract(:request, + from: "ejabberd_http.hrl") + + setup_all do + try do + :stringprep.start + rescue + _ -> :ok + end + :mod_http_api.start(@domain, []) + EjabberdOauthMock.init + :ok + end + + setup do + :meck.unload + :meck.new :ejabberd_commands + EjabberdAuthMock.init + :ok + end + + test "HTTP GET simple command call with Basic Auth" do + EjabberdAuthMock.create_user @user, @domain, @userpass + + # Mock a simple command() -> :ok + :meck.expect(:ejabberd_commands, :get_command_format, + fn (@acommand, {@user, @domain, @userpass, false}, @version) -> + {[], {:res, :rescode}} + end) + :meck.expect(:ejabberd_commands, :execute_command, + fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) -> + :ok + end) + + #:ejabberd_logger.start + #:ejabberd_logger.set 5 + + # Correct Basic Auth call + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # Basic auth + auth: {@user<>"@"<>@domain, @userpass}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 200 == elem(result, 0) # HTTP code + assert "0" == elem(result, 2) # command result + + # Bad password + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # Basic auth + auth: {@user<>"@"<>@domain, @userpass<>"bad"}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 401 == elem(result, 0) # HTTP code + + # Check that the command was executed only once + assert 1 == + :meck.num_calls(:ejabberd_commands, :execute_command, :_) + + assert :meck.validate :ejabberd_auth + assert :meck.validate :ejabberd_commands + #assert :ok = :meck.history(:ejabberd_commands) + end + + + test "HTTP GET simple command call with OAuth" do + EjabberdAuthMock.create_user @user, @domain, @userpass + + # Mock a simple command() -> :ok + :meck.expect(:ejabberd_commands, :get_command_format, + fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> + {[], {:res, :rescode}} + end) + :meck.expect(:ejabberd_commands, :execute_command, + fn (:undefined, {@user, @domain, {:oauth, _token}, false}, + @acommand, [], @version) -> + :ok + end) + + #:ejabberd_logger.start + #:ejabberd_logger.set 5 + + # Correct OAuth call + token = EjabberdOauthMock.get_token @user, @domain, @command + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # OAuth + auth: {:oauth, token, []}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 200 == elem(result, 0) # HTTP code + assert "0" == elem(result, 2) # command result + + # Wrong OAuth token + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # OAuth + auth: {:oauth, "bad"<>token, []}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 401 == elem(result, 0) # HTTP code + + # Expired OAuth token + token = EjabberdOauthMock.get_token @user, @domain, @command, 1 + :timer.sleep 1500 + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # OAuth + auth: {:oauth, token, []}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 401 == elem(result, 0) # HTTP code + + # Wrong OAuth scope + token = EjabberdOauthMock.get_token @user, @domain, "bad_command" + :timer.sleep 1500 + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # OAuth + auth: {:oauth, token, []}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 401 == elem(result, 0) # HTTP code + + # Check that the command was executed only once + assert 1 == + :meck.num_calls(:ejabberd_commands, :execute_command, :_) + + assert :meck.validate :ejabberd_auth + assert :meck.validate :ejabberd_commands + #assert :ok = :meck.history(:ejabberd_commands) + end + + +end diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index ae62f28f9..2ec33bcbb 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -1,6 +1,6 @@ # ---------------------------------------------------------------------- # -# ejabberd, Copyright (C) 2002-2015 ProcessOne +# ejabberd, Copyright (C) 2002-2016 ProcessOne # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -19,170 +19,78 @@ # ---------------------------------------------------------------------- defmodule ModHttpApiTest do - use ExUnit.Case, async: false + @author "mremond@process-one.net" - @author "jsautret@process-one.net" + use ExUnit.Case, async: true - # Admin user - @admin "admin" - @adminpass "adminpass" - # Non admin user - @user "user" - @userpass "userpass" - # XMPP domain - @domain "domain" - # mocked command - @command "command_test" - @acommand String.to_atom(@command) - # default API version - @version 0 - - require Record - Record.defrecord :request, Record.extract(:request, - from: "ejabberd_http.hrl") + require Record + Record.defrecord :request, Record.extract(:request, from_lib: "ejabberd/include/ejabberd_http.hrl") + Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - setup_all do - try do - :stringprep.start - rescue - _ -> :ok - end - :mod_http_api.start(@domain, []) - EjabberdOauthMock.init - :ok - end + setup_all do + :ok = :mnesia.start + :ok = :ejabberd_config.start(["localhost"], []) - setup do - :meck.unload - :meck.new :ejabberd_commands - EjabberdAuthMock.init - :ok - end + :ok = :ejabberd_commands.init - test "HTTP GET simple command call with Basic Auth" do - EjabberdAuthMock.create_user @user, @domain, @userpass + :ok = :ejabberd_commands.register_commands(cmds) + on_exit fn -> unregister_commands(cmds) end + end - # Mock a simple command() -> :ok - :meck.expect(:ejabberd_commands, :get_command_format, - fn (@acommand, {@user, @domain, @userpass, false}, @version) -> - {[], {:res, :rescode}} - end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) -> - :ok - end) + test "We can expose several commands to API at a time" do + :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]]) + commands = :ejabberd_commands.get_commands() + assert Enum.member?(commands, :open_cmd) + assert Enum.member?(commands, :user_cmd) + end - #:ejabberd_logger.start - #:ejabberd_logger.set 5 + test "We can call open commands without authentication" do + :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]]) + request = request(method: :POST, data: "[]") + {200, _, _} = :mod_http_api.process(["open_cmd"], request) + end - # Correct Basic Auth call - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # Basic auth - auth: {@user<>"@"<>@domain, @userpass}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 200 == elem(result, 0) # HTTP code - assert "0" == elem(result, 2) # command result + # This related to the commands config file option + test "Attempting to access a command that is not exposed as HTTP API returns 401" do + :ejabberd_config.add_local_option(:commands, []) + request = request(method: :POST, data: "[]") + {401, _, _} = :mod_http_api.process(["open_cmd"], request) + end - # Bad password - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # Basic auth - auth: {@user<>"@"<>@domain, @userpass<>"bad"}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 401 == elem(result, 0) # HTTP code + test "Call to user, admin or restricted commands without authentication are rejected" do + :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]]) + request = request(method: :POST, 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) + end - # Check that the command was executed only once - assert 1 == - :meck.num_calls(:ejabberd_commands, :execute_command, :_) + # Define a set of test commands that we expose through API + # We define one for each policy type + defp cmds do + [:open, :user, :admin, :restricted] + |> Enum.map(&({&1, String.to_atom(to_string(&1) <> "_cmd")})) + |> Enum.map(fn({cmd_type, cmd}) -> + ejabberd_commands(name: cmd, tags: [:test], + policy: cmd_type, + module: __MODULE__, + function: cmd, + args: [], + result: {:res, :rescode}) + end) + end - assert :meck.validate :ejabberd_auth - assert :meck.validate :ejabberd_commands - #assert :ok = :meck.history(:ejabberd_commands) - end + def open_cmd, do: :ok + def user_cmd, do: :ok + def admin_cmd, do: :ok + def restricted_cmd, do: :ok + defp unregister_commands(commands) do + try do + :ejabberd_commands.unregister_commands(commands) + catch + _,_ -> :ok + end + end - test "HTTP GET simple command call with OAuth" do - EjabberdAuthMock.create_user @user, @domain, @userpass - - # Mock a simple command() -> :ok - :meck.expect(:ejabberd_commands, :get_command_format, - fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> - {[], {:res, :rescode}} - end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, {:oauth, _token}, false}, - @acommand, [], @version) -> - :ok - end) - - #:ejabberd_logger.start - #:ejabberd_logger.set 5 - - # Correct OAuth call - token = EjabberdOauthMock.get_token @user, @domain, @command - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # OAuth - auth: {:oauth, token, []}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 200 == elem(result, 0) # HTTP code - assert "0" == elem(result, 2) # command result - - # Wrong OAuth token - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # OAuth - auth: {:oauth, "bad"<>token, []}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 401 == elem(result, 0) # HTTP code - - # Expired OAuth token - token = EjabberdOauthMock.get_token @user, @domain, @command, 1 - :timer.sleep 1500 - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # OAuth - auth: {:oauth, token, []}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 401 == elem(result, 0) # HTTP code - - # Wrong OAuth scope - token = EjabberdOauthMock.get_token @user, @domain, "bad_command" - :timer.sleep 1500 - req = request(method: :GET, - path: ["api", @command], - q: [nokey: ""], - # OAuth - auth: {:oauth, token, []}, - ip: {{127,0,0,1},60000}, - host: @domain) - result = :mod_http_api.process([@command], req) - assert 401 == elem(result, 0) # HTTP code - - # Check that the command was executed only once - assert 1 == - :meck.num_calls(:ejabberd_commands, :execute_command, :_) - - assert :meck.validate :ejabberd_auth - assert :meck.validate :ejabberd_commands - #assert :ok = :meck.history(:ejabberd_commands) - end - - end