mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-20 16:15:59 +01:00
Merge pull request #1211 from processone/expand_api
There is still work to do, be we reached a stable state and can merge up to this point.
This commit is contained in:
commit
7a74a4836a
@ -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{
|
||||
|
@ -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
|
||||
|
6
mix.exs
6
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
|
||||
|
9
mix.lock
9
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]}]}}
|
||||
|
11
src/acl.erl
11
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().
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) ->
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
26
src/jid.erl
26
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) ->
|
||||
|
@ -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]);
|
||||
|
@ -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) ->
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
7
test/test_helper.exs
Normal file
7
test/test_helper.exs
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user