25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00

Initial attempt on access on commands

May change and will require more work / test / refactor
This commit is contained in:
Mickael Remond 2016-07-25 11:43:49 +02:00
parent b4a430541d
commit d7ad99f147
No known key found for this signature in database
GPG Key ID: E6F6045D79965AA3
5 changed files with 166 additions and 96 deletions

View File

@ -28,6 +28,23 @@
-type oauth_scope() :: atom(). -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, -record(ejabberd_commands,
{name :: atom(), {name :: atom(),
tags = [] :: [atom()] | '_' | '$2', tags = [] :: [atom()] | '_' | '$2',
@ -38,7 +55,8 @@
function :: atom() | '_', function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2', args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user, policy = restricted :: open | restricted | admin | user,
access_rules = [] :: [atom()], %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
access = [] :: [{atom(),atom(),atom()}|atom()],
result = {res, rescode} :: rterm() | '_' | '$2', result = {res, rescode} :: rterm() | '_' | '$2',
args_desc = none :: none | [string()] | '_', args_desc = none :: none | [string()] | '_',
result_desc = none :: none | string() | '_', result_desc = none :: none | string() | '_',
@ -55,7 +73,7 @@
function :: atom(), function :: atom(),
args :: [aterm()], args :: [aterm()],
policy :: open | restricted | admin | user, policy :: open | restricted | admin | user,
access_rules :: [atom()], access :: [{atom(),atom(),atom()}|atom()],
result :: rterm()}. result :: rterm()}.
%% @type ejabberd_commands() = #ejabberd_commands{ %% @type ejabberd_commands() = #ejabberd_commands{

View File

@ -130,7 +130,7 @@ get_commands_spec() ->
#ejabberd_commands{name = register, tags = [accounts], #ejabberd_commands{name = register, tags = [accounts],
desc = "Register a user", desc = "Register a user",
policy = admin, policy = admin,
access_rules = [configure], access = [{mod_register, access, configure}],
module = ?MODULE, function = register, module = ?MODULE, function = register,
args = [{user, binary}, {host, binary}, {password, binary}], args = [{user, binary}, {host, binary}, {password, binary}],
result = {res, restuple}}, result = {res, restuple}},

View File

@ -319,8 +319,8 @@ list_commands() ->
list_commands(Version) -> list_commands(Version) ->
Commands = get_commands_definition(Version), Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name, [{Name, Args, Desc} || #ejabberd_commands{name = Name,
args = Args, args = Args,
desc = Desc} <- Commands]. desc = Desc} <- Commands].
-spec list_commands_policy(integer()) -> -spec list_commands_policy(integer()) ->
@ -331,10 +331,10 @@ list_commands(Version) ->
list_commands_policy(Version) -> list_commands_policy(Version) ->
Commands = get_commands_definition(Version), Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} || [{Name, Args, Desc, Policy} ||
#ejabberd_commands{name = Name, #ejabberd_commands{name = Name,
args = Args, args = Args,
desc = Desc, desc = Desc,
policy = Policy} <- Commands]. policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}. -spec get_command_format(atom()) -> {[aterm()], rterm()}.
@ -356,14 +356,14 @@ get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}), Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args, #ejabberd_commands{args = Args,
result = Result, result = Result,
policy = Policy} = policy = Policy} =
get_command_definition(Name, Version), get_command_definition(Name, Version),
case Policy of case Policy of
user when Admin; user when Admin;
Auth == noauth -> Auth == noauth ->
{[{user, binary}, {server, binary} | Args], Result}; {[{user, binary}, {server, binary} | Args], Result};
_ -> _ ->
{Args, Result} {Args, Result}
end. end.
-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}. -spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
@ -394,16 +394,16 @@ get_command_definition(Name) ->
%% @doc Get the definition record of a command in a given API version. %% @doc Get the definition record of a command in a given API version.
get_command_definition(Name, Version) -> get_command_definition(Name, Version) ->
case lists:reverse( case lists:reverse(
lists:sort( lists:sort(
mnesia:dirty_select( mnesia:dirty_select(
ejabberd_commands, ejabberd_commands,
ets:fun2ms( ets:fun2ms(
fun(#ejabberd_commands{name = N, version = V} = C) fun(#ejabberd_commands{name = N, version = V} = C)
when N == Name, V =< Version -> when N == Name, V =< Version ->
{V, C} {V, C}
end)))) of end)))) of
[{_, Command} | _ ] -> Command; [{_, Command} | _ ] -> Command;
_E -> throw(unknown_command) _E -> throw(unknown_command)
end. end.
-spec get_commands_definition(integer()) -> [ejabberd_commands()]. -spec get_commands_definition(integer()) -> [ejabberd_commands()].
@ -411,20 +411,20 @@ get_command_definition(Name, Version) ->
% @doc Returns all commands for a given API version % @doc Returns all commands for a given API version
get_commands_definition(Version) -> get_commands_definition(Version) ->
L = lists:reverse( L = lists:reverse(
lists:sort( lists:sort(
mnesia:dirty_select( mnesia:dirty_select(
ejabberd_commands, ejabberd_commands,
ets:fun2ms( ets:fun2ms(
fun(#ejabberd_commands{name = Name, version = V} = C) fun(#ejabberd_commands{name = Name, version = V} = C)
when V =< Version -> when V =< Version ->
{Name, V, C} {Name, V, C}
end)))), end)))),
F = fun({_Name, _V, Command}, []) -> F = fun({_Name, _V, Command}, []) ->
[Command]; [Command];
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
Acc; Acc;
({_Name, _V, Command}, Acc) -> [Command | Acc] ({_Name, _V, Command}, Acc) -> [Command | Acc]
end, end,
lists:foldl(F, [], L). lists:foldl(F, [], L).
%% @spec (Name::atom(), Arguments) -> ResultTerm %% @spec (Name::atom(), Arguments) -> ResultTerm
@ -433,7 +433,7 @@ get_commands_definition(Version) ->
%% @doc Execute a command. %% @doc Execute a command.
%% Can return the following exceptions: %% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | %% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided %% no_auth_provided | access_rules_unauthorized
execute_command(Name, Arguments) -> execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION). execute_command(Name, Arguments, ?DEFAULT_VERSION).
@ -505,7 +505,7 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
end, end,
TokenJID = oauth_token_user(Auth1), TokenJID = oauth_token_user(Auth1),
Command = get_command_definition(Name, Version), 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 case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
end. end.
@ -530,15 +530,22 @@ execute_check_policy(
{User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
execute_check_access(JID, Command, [User, Server | Arguments]). execute_check_access(JID, Command, [User, Server | Arguments]).
execute_check_access(_FromJID, #ejabberd_commands{access_rules = []} = Command, Arguments) -> execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
do_execute_command(Command, Arguments); do_execute_command(Command, Arguments);
execute_check_access(FromJID, #ejabberd_commands{access_rules = Rules} = 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 ? %% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
case acl:any_rules_allowed(global, Rules, FromJID) of 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 -> true ->
do_execute_command(Command, Arguments); do_execute_command(Command, Arguments);
false -> false ->
{error, access_rules_unauthorized} throw({error, access_rules_unauthorized})
end. end.
do_execute_command(Command, Arguments) -> do_execute_command(Command, Arguments) ->
@ -611,31 +618,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
Command1 Command1
end, end,
AccessCommandsAllowed = AccessCommandsAllowed =
lists:filter( lists:filter(
fun({Access, Commands, ArgumentRestrictions}) -> fun({Access, Commands, ArgumentRestrictions}) ->
case check_access(Command, Access, Auth, CallerInfo) of case check_access(Command, Access, Auth, CallerInfo) of
true -> true ->
check_access_command(Commands, Command, check_access_command(Commands, Command,
ArgumentRestrictions, ArgumentRestrictions,
Method, Arguments); Method, Arguments);
false -> false ->
false false
end; end;
({Access, Commands}) -> ({Access, Commands}) ->
ArgumentRestrictions = [], ArgumentRestrictions = [],
case check_access(Command, Access, Auth, CallerInfo) of case check_access(Command, Access, Auth, CallerInfo) of
true -> true ->
check_access_command(Commands, Command, check_access_command(Commands, Command,
ArgumentRestrictions, ArgumentRestrictions,
Method, Arguments); Method, Arguments);
false -> false ->
false false
end end
end, end,
AccessCommands), AccessCommands),
case AccessCommandsAllowed of case AccessCommandsAllowed of
[] -> throw({error, account_unprivileged}); [] -> throw({error, account_unprivileged});
L when is_list(L) -> ok L when is_list(L) -> ok
end. end.
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided; -spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
@ -699,9 +706,9 @@ check_access2(Access, AccessInfo, Server) ->
check_access_command(Commands, Command, ArgumentRestrictions, check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments) -> Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of case Commands==all orelse lists:member(Method, Commands) of
true -> check_access_arguments(Command, ArgumentRestrictions, true -> check_access_arguments(Command, ArgumentRestrictions,
Arguments); Arguments);
false -> false false -> false
end. end.
check_access_arguments(Command, ArgumentRestrictions, Arguments) -> check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
@ -724,6 +731,10 @@ tag_arguments(ArgsDefs, Args) ->
Args). Args).
%% Get commands for all version
get_all_access_commands(AccessCommands) ->
get_access_commands(AccessCommands, ?DEFAULT_VERSION).
get_access_commands(undefined, Version) -> get_access_commands(undefined, Version) ->
Cmds = get_commands(Version), Cmds = get_commands(Version),
[{?POLICY_ACCESS, Cmds, []}]; [{?POLICY_ACCESS, Cmds, []}];
@ -736,7 +747,7 @@ get_commands(Version) ->
Opts0 = ejabberd_config:get_option( Opts0 = ejabberd_config:get_option(
commands, commands,
fun(V) when is_list(V) -> V end, fun(V) when is_list(V) -> V end,
[]), []),
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0), Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
CommandsList = list_commands_policy(Version), CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList], OpenCmds = [N || {N, _, _, open} <- CommandsList],
@ -746,27 +757,29 @@ get_commands(Version) ->
Cmds = Cmds =
lists:foldl( lists:foldl(
fun([{add_commands, L}], Acc) -> fun([{add_commands, L}], Acc) ->
Cmds = case L of Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
lists:usort(Cmds ++ Acc); lists:usort(Cmds ++ Acc);
([{remove_commands, L}], Acc) -> ([{remove_commands, L}], Acc) ->
Cmds = case L of Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ when is_list(L) -> L
end,
Acc -- Cmds; Acc -- Cmds;
(_, Acc) -> Acc (_, Acc) -> Acc
end, AdminCmds ++ UserCmds, Opts), end, [], Opts),
Cmds. Cmds.
expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
lists:foldl(fun(El, Acc) ->
expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) ++ Acc
end, [], L);
expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) ->
case El of
open -> OpenCmds;
restricted -> RestrictedCmds;
admin -> AdminCmds;
user -> UserCmds;
_ -> [El]
end.
oauth_token_user(noauth) -> oauth_token_user(noauth) ->
undefined; undefined;
oauth_token_user(admin) -> oauth_token_user(admin) ->

View File

@ -136,8 +136,7 @@ check_permissions(Request, Command) ->
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
check_permissions2(Request, Call, CommandPolicy, Scope); check_permissions2(Request, Call, CommandPolicy, Scope);
_ -> _ ->
%% TODO Should this be a 404 or 400 instead of 401 ? json_error(404, 40, <<"Endpoint not found.">>)
unauthorized_response()
end. end.
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList) check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
@ -269,10 +268,10 @@ get_api_version(#request{path = Path}) ->
get_api_version(lists:reverse(Path)); get_api_version(lists:reverse(Path));
get_api_version([<<"v", String/binary>> | Tail]) -> get_api_version([<<"v", String/binary>> | Tail]) ->
case catch jlib:binary_to_integer(String) of case catch jlib:binary_to_integer(String) of
N when is_integer(N) -> N when is_integer(N) ->
N; N;
_ -> _ ->
get_api_version(Tail) get_api_version(Tail)
end; end;
get_api_version([_Head | Tail]) -> get_api_version([_Head | Tail]) ->
get_api_version(Tail); get_api_version(Tail);
@ -318,6 +317,8 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{401, iolist_to_binary(Msg)}; {401, iolist_to_binary(Msg)};
throw:{error, account_unprivileged} -> throw:{error, account_unprivileged} ->
{403, 31, <<"Command need to be run with admin priviledge.">>}; {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} -> throw:{invalid_parameter, Msg} ->
{400, iolist_to_binary(Msg)}; {400, iolist_to_binary(Msg)};
throw:{error, Why} when is_atom(Why) -> throw:{error, Why} when is_atom(Why) ->

View File

@ -18,6 +18,8 @@
# #
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
## TODO Fix next test error: add admin user ACL
defmodule EjabberdCommandsMockTest do defmodule EjabberdCommandsMockTest do
use ExUnit.Case, async: false use ExUnit.Case, async: false
@ -44,6 +46,9 @@ defmodule EjabberdCommandsMockTest do
_ -> :ok _ -> :ok
end end
:mnesia.start :mnesia.start
:ok = :jid.start
:ok = :ejabberd_config.start(["domain1", "domain2"], [])
:ok = :acl.start
EjabberdOauthMock.init EjabberdOauthMock.init
:ok :ok
end end
@ -313,7 +318,6 @@ defmodule EjabberdCommandsMockTest do
end end
test "API command with admin policy" do test "API command with admin policy" do
mock_commands_config mock_commands_config
@ -393,6 +397,40 @@ defmodule EjabberdCommandsMockTest do
assert :meck.validate @module assert :meck.validate @module
end end
test "Commands can perform extra check on access" do
mock_commands_config
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})
: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 # Utils
@ -412,7 +450,7 @@ defmodule EjabberdCommandsMockTest do
end) end)
:meck.expect(:ejabberd_config, :get_myhosts, :meck.expect(:ejabberd_config, :get_myhosts,
fn() -> [@domain] end) fn() -> [@domain] end)
:meck.new :acl :meck.new :acl #, [:passthrough]
:meck.expect(:acl, :access_matches, :meck.expect(:acl, :access_matches,
fn(:commands_admin_access, info, _scope) -> fn(:commands_admin_access, info, _scope) ->
case info do case info do