aboutsummaryrefslogtreecommitdiff
path: root/src/ejabberd_commands.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/ejabberd_commands.erl')
-rw-r--r--src/ejabberd_commands.erl503
1 files changed, 81 insertions, 422 deletions
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index d5649b2d7..08faff22e 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -5,7 +5,7 @@
%%% Created : 20 May 2008 by Badlop <badlop@process-one.net>
%%%
%%%
-%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%% ejabberd, Copyright (C) 2002-2019 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
@@ -200,7 +200,7 @@
%%% command.
%%% TODO: consider this feature:
-%%% All commands are catched. If an error happens, return the restuple:
+%%% All commands are caught. If an error happens, return the restuple:
%%% {error, flattened error string}
%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc)
%%% need to allows this. And ejabberd_xmlrpc must be prepared to
@@ -210,39 +210,41 @@
-module(ejabberd_commands).
-author('badlop@process-one.net').
+-behaviour(gen_server).
+
-define(DEFAULT_VERSION, 1000000).
--export([init/0,
+-export([start_link/0,
list_commands/0,
list_commands/1,
get_command_format/1,
get_command_format/2,
get_command_format/3,
- get_command_policy_and_scope/1,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
- get_exposed_commands/0,
register_commands/1,
- unregister_commands/1,
- expose_commands/1,
- execute_command/2,
- execute_command/3,
- execute_command/4,
- execute_command/5,
- execute_command/6,
- opt_type/1,
- get_commands_spec/0
- ]).
+ unregister_commands/1,
+ get_commands_spec/0,
+ get_commands_definition/0,
+ get_commands_definition/1,
+ execute_command2/3,
+ execute_command2/4]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
-include("ejabberd_commands.hrl").
--include("ejabberd.hrl").
-include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-define(POLICY_ACCESS, '$policy').
+-type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map().
+
+-record(state, {}).
+
get_commands_spec() ->
[
#ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation],
@@ -254,9 +256,9 @@ get_commands_spec() ->
"documentation should be stored",
"Regexp matching names of commands or modules "
"that will be included inside generated document",
- "Comma separated list of languages (choosen from java, perl, xmlrpc, json)"
+ "Comma separated list of languages (chosen from java, perl, xmlrpc, json)"
"that will have example invocation include in markdown document"],
- result_desc = "0 if command failed, 1 when succedded",
+ result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok},
#ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation],
@@ -268,19 +270,45 @@ get_commands_spec() ->
"documentation should be stored",
"Regexp matching names of commands or modules "
"that will be included inside generated document",
- "Comma separated list of languages (choosen from java, perl, xmlrpc, json)"
+ "Comma separated list of languages (chosen from java, perl, xmlrpc, json)"
"that will have example invocation include in markdown document"],
- result_desc = "0 if command failed, 1 when succedded",
+ result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
-init() ->
- mnesia:create_table(ejabberd_commands,
+
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+init([]) ->
+ try mnesia:transform_table(ejabberd_commands, ignore,
+ record_info(fields, ejabberd_commands))
+ catch exit:{aborted, {no_exists, _}} -> ok
+ end,
+ ejabberd_mnesia:create(?MODULE, ejabberd_commands,
[{ram_copies, [node()]},
{local_content, true},
{attributes, record_info(fields, ejabberd_commands)},
{type, bag}]),
- mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
- register_commands(get_commands_spec()).
+ register_commands(get_commands_spec()),
+ {ok, #state{}}.
+
+handle_call(Request, From, State) ->
+ ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
+ {noreply, State}.
+
+handle_cast(Msg, State) ->
+ ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
+ {noreply, State}.
+
+handle_info(Info, State) ->
+ ?WARNING_MSG("Unexpected info: ~p", [Info]),
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
-spec register_commands([ejabberd_commands()]) -> ok.
@@ -296,7 +324,9 @@ register_commands(Commands) ->
mnesia:dirty_write(Command)
%% ?DEBUG("This command is already defined:~n~p", [Command])
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
-spec unregister_commands([ejabberd_commands()]) -> ok.
@@ -306,26 +336,8 @@ unregister_commands(Commands) ->
fun(Command) ->
mnesia:dirty_delete_object(Command)
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.
+ Commands),
+ ejabberd_access_permissions:invalidate().
-spec list_commands() -> [{atom(), [aterm()], string()}].
@@ -336,28 +348,14 @@ list_commands() ->
-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
%% @doc Get a list of all the available commands, arguments and
-%% description in a given API verion.
+%% description in a given API version.
list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
args = Args,
desc = Desc} <- Commands].
-
--spec list_commands_policy(integer()) ->
- [{atom(), [aterm()], string(), atom()}].
-
-%% @doc Get a list of all the available commands, arguments,
-%% description, and policy in a given API 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].
-
--spec get_command_format(atom()) -> {[aterm()], rterm()}.
+-spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}.
%% @doc Get the format of arguments and result of a command.
get_command_format(Name) ->
@@ -367,43 +365,22 @@ get_command_format(Name, Version) when is_integer(Version) ->
get_command_format(Name, Auth) ->
get_command_format(Name, Auth, ?DEFAULT_VERSION).
--spec get_command_format(atom(),
- {binary(), binary(), binary(), boolean()} |
- noauth | admin,
- integer()) ->
- {[aterm()], rterm()}.
-
+-spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(),atom()}], rterm()}.
get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args,
result = Result,
+ args_rename = Rename,
policy = Policy} =
get_command_definition(Name, Version),
case Policy of
user when Admin;
Auth == noauth ->
- {[{user, binary}, {server, binary} | Args], Result};
+ {[{user, binary}, {host, binary} | Args], Rename, Result};
_ ->
- {Args, Result}
+ {Args, Rename, Result}
end.
--spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
-
-%% @doc return command policy.
-get_command_policy_and_scope(Name) ->
- case get_command_definition(Name) of
- #ejabberd_commands{policy = Policy} = Cmd ->
- {ok, Policy, cmd_scope(Cmd)};
- command_not_found ->
- {error, command_not_found}
- end.
-
-%% The oauth scopes for a command are the command name itself,
-%% also might include either 'ejabberd:user' or 'ejabberd:admin'
-cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
- [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
-
-
-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
@@ -427,6 +404,9 @@ get_command_definition(Name, Version) ->
_E -> throw({error, unknown_command})
end.
+get_commands_definition() ->
+ get_commands_definition(?DEFAULT_VERSION).
+
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
% @doc Returns all commands for a given API version
@@ -448,134 +428,24 @@ get_commands_definition(Version) ->
end,
lists:foldl(F, [], L).
-%% @spec (Name::atom(), Arguments) -> ResultTerm
-%% where
-%% Arguments = [any()]
-%% @doc Execute a command.
-%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data |
-%% no_auth_provided | access_rules_unauthorized
-execute_command(Name, Arguments) ->
- execute_command(Name, Arguments, ?DEFAULT_VERSION).
-
--spec execute_command(atom(),
- [any()],
- integer() |
- {binary(), binary(), binary(), boolean()} |
- noauth | admin
- ) -> any().
-
-%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm
-%% where
-%% Auth = {User::string(), Server::string(), Password::string(),
-%% Admin::boolean()}
-%% | noauth
-%% | admin
-%% Arguments = [any()]
-%%
-%% @doc Execute a command in a given API version
-%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data |
-%% no_auth_provided
-execute_command(Name, Arguments, Version) when is_integer(Version) ->
- execute_command([], noauth, Name, Arguments, Version);
-execute_command(Name, Arguments, Auth) ->
- execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION).
-
-%% @spec (AccessCommands, Auth, Name::atom(), Arguments) ->
-%% ResultTerm | {error, Error}
-%% where
-%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
-%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
-%% | noauth
-%% | admin
-%% Arguments = [any()]
-%%
-%% @doc Execute a command
-%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
-execute_command(AccessCommands, Auth, Name, Arguments) ->
- execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION).
-
--spec execute_command([{atom(), [atom()], [any()]}] | undefined,
- {binary(), binary(), binary(), boolean()} |
- noauth | admin,
- atom(),
- [any()],
- integer()
- ) -> any().
-
-%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm
-%% where
-%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
-%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
-%% | noauth
-%% | admin
-%% Arguments = [any()]
-%%
-%% @doc Execute a command in a given API version
-%% Can return the following exceptions:
-%% 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, 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_all_access_commands(AccessCommands1),
+execute_command2(Name, Arguments, CallerInfo) ->
+ execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
- case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
- ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
+execute_command2(Name, Arguments, CallerInfo, Version) ->
+ Command = get_command_definition(Name, Version),
+ case ejabberd_access_permissions:can_access(Name, CallerInfo) of
+ allow ->
+ do_execute_command(Command, Arguments);
+ _ ->
+ throw({error, access_rules_unauthorized})
end.
-execute_check_policy(
- _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
- do_execute_command(Command, Arguments);
-execute_check_policy(
- noauth, _JID, 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(
- {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(undefined, _Command, _Arguments) ->
- throw({error, access_rules_unauthorized});
-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]),
+ ejabberd_hooks:run(api_call, [Module, Function, Arguments]),
apply(Module, Function, Arguments).
-spec get_tags_commands() -> [{string(), [string()]}].
@@ -616,223 +486,12 @@ get_tags_commands(Version) ->
%% -----------------------------
%% Access verification
%% -----------------------------
-
-%% @spec (AccessCommands, Auth, Method, Command, Arguments) -> ok
-%% where
-%% AccessCommands = [ {Access, CommandNames, Arguments} ]
-%% Auth = {User::string(), Server::string(), Password::string()} | noauth
-%% Method = atom()
-%% Arguments = [any()]
-%% @doc Check access is allowed to that command.
-%% At least one AccessCommand must be satisfied.
-%% It may throw {error, Error} where:
-%% Error = account_unprivileged | invalid_account_data
-check_access_commands([], _Auth, _Method, _Command, _Arguments, _CallerInfo) ->
- ok;
-check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerInfo) ->
- Command =
- case {Command1#ejabberd_commands.policy, Auth} of
- {user, {_, _, _, _}} ->
- Command1;
- {user, _} ->
- Command1#ejabberd_commands{
- args = [{user, binary}, {server, binary} |
- Command1#ejabberd_commands.args]};
- _ ->
- 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),
- case AccessCommandsAllowed of
- [] -> throw({error, account_unprivileged});
- L when is_list(L) -> ok
- end.
-
--spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
- (ejabberd_commands(),
- {binary(), binary(), binary(), boolean()}) ->
- {ok, binary(), binary()}.
-
-check_auth(_Command, noauth) ->
- no_auth_provided;
-check_auth(Command, {User, Server, {oauth, Token}, _}) ->
- ScopeList = cmd_scope(Command),
- case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
- true ->
- {ok, User, Server};
- _ ->
- throw({error, invalid_account_data})
- end;
-check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) ->
- %% Check the account exists and password is valid
- case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
- true -> {ok, User, Server};
- _ -> throw({error, invalid_account_data})
- end.
-
-check_access(Command, ?POLICY_ACCESS, _, _)
- when Command#ejabberd_commands.policy == open ->
- true;
-check_access(_Command, _Access, admin, _) ->
- true;
-check_access(_Command, _Access, {_User, _Server, _, true}, _) ->
- false;
-check_access(Command, Access, Auth, CallerInfo)
- when Access =/= ?POLICY_ACCESS;
- Command#ejabberd_commands.policy == open;
- Command#ejabberd_commands.policy == user ->
- case check_auth(Command, Auth) of
- {ok, User, Server} ->
- check_access2(Access, CallerInfo#{usr => jid:split(jid:make(User, Server, <<>>))}, Server);
- no_auth_provided ->
- case Command#ejabberd_commands.policy of
- user ->
- false;
- _ ->
- check_access2(Access, CallerInfo, global)
- end;
- _ ->
- false
- end;
-check_access(_Command, _Access, _Auth, _CallerInfo) ->
- false.
-
-check_access2(?POLICY_ACCESS, _CallerInfo, _Server) ->
- true;
-check_access2(Access, AccessInfo, Server) ->
- %% Check this user has access permission
- case acl:access_matches(Access, AccessInfo, Server) of
- allow -> true;
- deny -> false
- end.
-
-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
- end.
-
-check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
- ArgumentsTagged = tag_arguments(Command#ejabberd_commands.args, Arguments),
- lists:all(
- fun({ArgName, ArgAllowedValue}) ->
- %% If the call uses the argument, check the value is acceptable
- case lists:keysearch(ArgName, 1, ArgumentsTagged) of
- {value, {ArgName, ArgValue}} -> ArgValue == ArgAllowedValue;
- false -> true
- end
- end, ArgumentRestrictions).
-
-tag_arguments(ArgsDefs, Args) ->
- lists:zipwith(
- fun({ArgName, _ArgType}, ArgValue) ->
- {ArgName, ArgValue}
- end,
- ArgsDefs,
- Args).
-
-
-%% Get commands for all version
-get_all_access_commands(AccessCommands) ->
- get_access_commands(AccessCommands, ?DEFAULT_VERSION).
-
-get_access_commands(undefined, Version) ->
- Cmds = get_exposed_commands(Version),
- [{?POLICY_ACCESS, Cmds, []}];
-get_access_commands(AccessCommands, _Version) ->
- AccessCommands.
-
-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],
- RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
- AdminCmds = [N || {N, _, _, admin} <- CommandsList],
- UserCmds = [N || {N, _, _, user} <- CommandsList],
- Cmds =
- lists:foldl(
- fun([{add_commands, L}], Acc) ->
- Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
- lists:usort(Cmds ++ Acc);
- ([{remove_commands, L}], Acc) ->
- Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
- Acc -- Cmds;
- (_, Acc) -> Acc
- 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, <<>>).
-
+-spec is_admin(atom(), admin | noauth | auth(), map()) -> boolean().
is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
false;
-is_admin(Name, Auth, Extra) ->
- {ACLInfo, Server} = case Auth of
- {U, S, _, _} ->
- {Extra#{usr=>jid:split(jid:make(U, S, <<>>))}, S};
- _ ->
- {Extra, global}
- end,
- AdminAccess = ejabberd_config:get_option(
- commands_admin_access,
- fun(V) -> V end,
- none),
- case acl:access_matches(AdminAccess, ACLInfo, Server) of
- allow ->
- case catch check_auth(get_command_definition(Name), Auth) of
- {ok, _, _} -> true;
- no_auth_provided -> true;
- _ -> false
- end;
- deny -> false
- end.
-
-opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
-opt_type(commands) ->
- fun(V) when is_list(V) -> V end;
-opt_type(_) -> [commands, commands_admin_access].
+is_admin(_Name, Map, _extra) when is_map(Map) ->
+ true;
+is_admin(_Name, _Auth, _Extra) ->
+ false.