diff options
Diffstat (limited to 'src/ejabberd_commands.erl')
-rw-r--r-- | src/ejabberd_commands.erl | 503 |
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. |