aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaweł Chmielowski <pchmielowski@process-one.net>2016-10-05 13:21:11 +0200
committerPaweł Chmielowski <pchmielowski@process-one.net>2016-10-05 13:21:11 +0200
commit98e0123ca48cc23f699b5c61598740fa6b8a320e (patch)
tree60a9b2c7ecb5c5c4a1fc46fa12a968bb04157103 /src
parentejabberd_sm: Clean up old offline session entries (diff)
New api permissions framework
Diffstat (limited to '')
-rw-r--r--src/acl.erl40
-rw-r--r--src/ejabberd_access_permissions.erl527
-rw-r--r--src/ejabberd_admin.erl3
-rw-r--r--src/ejabberd_app.erl1
-rw-r--r--src/ejabberd_commands.erl53
-rw-r--r--src/ejabberd_config.erl3
-rw-r--r--src/ejabberd_ctl.erl13
-rw-r--r--src/ejabberd_oauth.erl25
-rw-r--r--src/mod_http_api.erl195
9 files changed, 724 insertions, 136 deletions
diff --git a/src/acl.erl b/src/acl.erl
index 349198182..1476081dd 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -36,7 +36,8 @@
acl_rule_verify/1, access_matches/3,
transform_access_rules_config/1,
parse_ip_netmask/1,
- access_rules_validator/1, shaper_rules_validator/1]).
+ access_rules_validator/1, shaper_rules_validator/1,
+ normalize_spec/1, resolve_access/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -437,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
acl_rule_matches(_ACL, _Data, _Host) ->
false.
--spec access_matches(atom()|list(), any(), global|binary()) -> any().
-access_matches(all, _Data, _Host) ->
- allow;
-access_matches(none, _Data, _Host) ->
- deny;
-access_matches(Name, Data, Host) when is_atom(Name) ->
+resolve_access(all, _Host) ->
+ all;
+resolve_access(none, _Host) ->
+ none;
+resolve_access(Name, Host) when is_atom(Name) ->
GAccess = mnesia:dirty_read(access, {Name, global}),
LAccess =
- if Host /= global -> mnesia:dirty_read(access, {Name, Host});
- true -> []
- end,
+ if Host /= global -> mnesia:dirty_read(access, {Name, Host});
+ true -> []
+ end,
case GAccess ++ LAccess of
[] ->
- deny;
+ [];
AccessList ->
- Rules = lists:flatmap(
+ lists:flatmap(
fun(#access{rules = Rs}) ->
Rs
- end, AccessList),
- access_rules_matches(Rules, Data, Host)
+ end, AccessList)
end;
-access_matches(Rules, Data, Host) when is_list(Rules) ->
- access_rules_matches(Rules, Data, Host).
-
+resolve_access(Rules, _Host) when is_list(Rules) ->
+ Rules.
+
+-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
+access_matches(Rules, Data, Host) ->
+ case resolve_access(Rules, Host) of
+ all -> allow;
+ none -> deny;
+ RRules -> access_rules_matches(RRules, Data, Host)
+ end.
-spec access_rules_matches(list(), any(), global|binary()) -> any().
diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl
new file mode 100644
index 000000000..f37de9a13
--- /dev/null
+++ b/src/ejabberd_access_permissions.erl
@@ -0,0 +1,527 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_access_permissions.erl
+%%% Author : Paweł Chmielowski <pawel@process-one.net>
+%%% Purpose : Administrative functions and commands
+%%% Created : 7 Sep 2016 by Paweł Chmielowski <pawel@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%-------------------------------------------------------------------
+-module(ejabberd_access_permissions).
+-author("pawel@process-one.net").
+
+-include("ejabberd_commands.hrl").
+-include("logger.hrl").
+
+-behaviour(gen_server).
+-behavior(ejabberd_config).
+
+%% API
+-export([start_link/0,
+ parse_api_permissions/1,
+ can_access/2,
+ invalidate/0,
+ opt_type/1,
+ show_current_definitions/0,
+ register_permission_addon/2,
+ unregister_permission_addon/1]).
+
+%% gen_server callbacks
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {
+ definitions = none,
+ fragments_generators = []
+}).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+
+-spec can_access(atom(), map()) -> allow | deny.
+can_access(Cmd, CallerInfo) ->
+ gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
+
+-spec invalidate() -> ok.
+invalidate() ->
+ gen_server:cast(?MODULE, invalidate).
+
+-spec register_permission_addon(atom(), fun()) -> ok.
+register_permission_addon(Name, Fun) ->
+ gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}).
+
+-spec unregister_permission_addon(atom()) -> ok.
+unregister_permission_addon(Name) ->
+ gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}).
+
+-spec show_current_definitions() -> any().
+show_current_definitions() ->
+ gen_server:call(?MODULE, show_current_definitions).
+
+%%--------------------------------------------------------------------
+%% @doc
+%% Starts the server
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
+start_link() ->
+ gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Initializes the server
+%%
+%% @spec init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% @end
+%%--------------------------------------------------------------------
+-spec init(Args :: term()) ->
+ {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term()} | ignore.
+init([]) ->
+ {ok, #state{}}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling call messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()},
+ State :: #state{}) ->
+ {reply, Reply :: term(), NewState :: #state{}} |
+ {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
+ CallerModule = maps:get(caller_module, CallerInfo, none),
+ Host = maps:get(caller_host, CallerInfo, global),
+ {State2, Defs} = get_definitions(State),
+ Res = lists:foldl(
+ fun({Name, _} = Def, none) ->
+ case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of
+ true ->
+ ?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
+ allow;
+ _ ->
+ none
+ end;
+ (_, Val) ->
+ Val
+ end, none, Defs),
+ Res2 = case Res of
+ allow -> allow;
+ _ ->
+ ?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]),
+ deny
+ end,
+ {reply, Res2, State2};
+handle_call(show_current_definitions, _From, State) ->
+ {State2, Defs} = get_definitions(State),
+ {reply, Defs, State2};
+handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keystore(Name, 1, Gens, {Name, Fun}),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) ->
+ NGens = lists:keydelete(Name, 1, Gens),
+ {reply, ok, State#state{fragments_generators = NGens}};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling cast messages
+%%
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_cast(Request :: term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_cast(invalidate, State) ->
+ {noreply, State#state{definitions = none}};
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Handling all non call/cast messages
+%%
+%% @spec handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% @end
+%%--------------------------------------------------------------------
+-spec handle_info(Info :: timeout() | term(), State :: #state{}) ->
+ {noreply, NewState :: #state{}} |
+ {noreply, NewState :: #state{}, timeout() | hibernate} |
+ {stop, Reason :: term(), NewState :: #state{}}.
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any
+%% necessary cleaning up. When it returns, the gen_server terminates
+%% with Reason. The return value is ignored.
+%%
+%% @spec terminate(Reason, State) -> void()
+%% @end
+%%--------------------------------------------------------------------
+-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
+ State :: #state{}) -> term().
+terminate(_Reason, _State) ->
+ ok.
+
+%%--------------------------------------------------------------------
+%% @private
+%% @doc
+%% Convert process state when code is changed
+%%
+%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% @end
+%%--------------------------------------------------------------------
+-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{},
+ Extra :: term()) ->
+ {ok, NewState :: #state{}} | {error, Reason :: term()}.
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+
+get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) ->
+ DefaultOptions = [{<<"console commands">>,
+ [ejabberd_ctl],
+ [{acl, all}],
+ {all, none}},
+ {<<"admin access">>,
+ [],
+ [{acl, admin}],
+ {all, [start, stop]}}],
+ NDefs = case Defs of
+ none ->
+ ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions),
+ AllCommands = ejabberd_commands:get_commands_definition(),
+ Frags = lists:foldl(
+ fun({_Name, Generator}, Acc) ->
+ Acc ++ Generator()
+ end, [], Gens),
+ lists:map(
+ fun({Name, {From, Who, {Add, Del}}}) ->
+ Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
+ {Name, {From, Who, Cmds}}
+ end, ApiPerms ++ Frags);
+ V ->
+ V
+ end,
+ {State#state{definitions = NDefs}, NDefs}.
+
+matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) ->
+ case lists:member(Cmd, What) of
+ true ->
+ case From == [] orelse lists:member(Module, From) of
+ true ->
+ Scope = maps:get(scope, CallerInfo, none),
+ lists:any(
+ fun({access, Access}) when Scope == none ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) when Scope == none ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host);
+ ({oauth, List}) when Scope /= none ->
+ lists:all(
+ fun({access, Access}) ->
+ acl:access_matches(Access, CallerInfo, Host) == allow;
+ ({acl, _} = Acl) ->
+ acl:acl_rule_matches(Acl, CallerInfo, Host);
+ ({scope, Scopes}) ->
+ ejabberd_oauth:scope_in_scope_list(Scope, Scopes)
+ end, List);
+ (_) ->
+ false
+ end, Who);
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end.
+
+filter_commands_with_permissions(AllCommands, Add, Del) ->
+ CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
+ CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []),
+ lists:map(fun(#ejabberd_commands{name = N}) -> N end,
+ CommandsAdd -- CommandsDel).
+
+filter_commands_with_patterns([], _Patterns, Acc) ->
+ Acc;
+filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
+ case command_matches_patterns(C, Patterns) of
+ true ->
+ filter_commands_with_patterns(CRest, Patterns, [C | Acc]);
+ _ ->
+ filter_commands_with_patterns(CRest, Patterns, Acc)
+ end.
+
+command_matches_patterns(_, all) ->
+ true;
+command_matches_patterns(_, none) ->
+ false;
+command_matches_patterns(_, []) ->
+ false;
+command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) ->
+ case lists:member(Tag, Tags) of
+ true ->
+ true;
+ _ ->
+ command_matches_patterns(C, Tail)
+ end;
+command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
+ true;
+command_matches_patterns(C, [_ | Tail]) ->
+ command_matches_patterns(C, Tail).
+
+%%%===================================================================
+%%% Options parsing code
+%%%===================================================================
+
+parse_api_permissions(Data) when is_list(Data) ->
+ throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}).
+
+parse_api_permission(Name, Args) ->
+ {From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of
+ {error, Msg} ->
+ report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]);
+ Val -> Val
+ end,
+ {Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}.
+
+parse_from(_Name, Module) when is_atom(Module) ->
+ [Module];
+parse_from(Name, Modules) when is_list(Modules) ->
+ lists:foreach(fun(Module) when is_atom(Module) ->
+ ok;
+ (Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name])
+ end, Modules),
+ Modules;
+parse_from(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_who(Name, Atom, ParseOauth) when is_atom(Atom) ->
+ parse_who(Name, [Atom], ParseOauth);
+parse_who(Name, Defs, ParseOauth) when is_list(Defs) ->
+ lists:map(
+ fun([{access, Val}]) ->
+ try acl:access_rules_validator(Val) of
+ Rule ->
+ {access, Rule}
+ catch
+ throw:{invalid_syntax, Msg} ->
+ report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>,
+ [Msg, Name]);
+ throw:{replace_with, NVal} ->
+ {access, NVal};
+ error:_ ->
+ report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name])
+ end;
+ ([{oauth, OauthList}]) when is_list(OauthList) ->
+ case ParseOauth of
+ oauth ->
+ {oauth, parse_who(Name, lists:flatten(OauthList), scope)};
+ scope ->
+ report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>,
+ [Name])
+ end;
+ ({scope, ScopeList}) ->
+ case ParseOauth of
+ oauth ->
+ report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>,
+ [Name]);
+ scope ->
+ ScopeList2 = case ScopeList of
+ V when is_binary(V) -> [V];
+ V2 when is_list(V2) -> V2;
+ V3 ->
+ report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>,
+ [V3, Name])
+ end,
+ {scope, ScopeList2}
+ end;
+ (Atom) when is_atom(Atom) ->
+ {acl, Atom};
+ ([Other]) ->
+ try acl:normalize_spec(Other) of
+ Rule2 ->
+ {acl, Rule2}
+ catch
+ _:_ ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Other, Name])
+ end;
+ (Invalid) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Invalid, Name])
+ end, Defs);
+parse_who(Name, Val, _ParseOauth) ->
+ report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_what(Name, Binary) when is_binary(Binary) ->
+ parse_what(Name, [Binary]);
+parse_what(Name, Defs) when is_list(Defs) ->
+ {A, D} = lists:foldl(
+ fun(Def, {Add, Del}) ->
+ case parse_single_what(Def) of
+ {error, Err} ->
+ report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>,
+ [Err, Def, Name]);
+ all ->
+ {case Add of none -> none; _ -> all end, Del};
+ {neg, all} ->
+ {none, all};
+ {neg, Value} ->
+ {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end};
+ Value ->
+ {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
+ end
+ end, {[], []}, Defs),
+ case {A, D} of
+ {[], _} ->
+ {none, all};
+ {A2, []} ->
+ {A2, none};
+ V ->
+ V
+ end;
+parse_what(Name, Val) ->
+ report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
+ [Val, Name]).
+
+parse_single_what(<<"*">>) ->
+ all;
+parse_single_what(<<"!*">>) ->
+ {neg, all};
+parse_single_what(<<"!", Rest/binary>>) ->
+ case parse_single_what(Rest) of
+ {neg, _} ->
+ {error, <<"Double negation">>};
+ {error, _} = Err ->
+ Err;
+ V ->
+ {neg, V}
+ end;
+parse_single_what(<<"[tag:", Rest/binary>>) ->
+ case binary:split(Rest, <<"]">>) of
+ [TagName, <<"">>] ->
+ case parse_single_what(TagName) of
+ {error, _} = Err ->
+ Err;
+ V when is_atom(V) ->
+ {tag, V};
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+ _ ->
+ {error, <<"Invalid tag">>}
+ end;
+parse_single_what(Binary) when is_binary(Binary) ->
+ case is_valid_command_name(Binary) of
+ true ->
+ binary_to_atom(Binary, latin1);
+ _ ->
+ {error, <<"Invalid value">>}
+ end;
+parse_single_what(_) ->
+ {error, <<"Invalid value">>}.
+
+is_valid_command_name(<<>>) ->
+ false;
+is_valid_command_name(Val) ->
+ is_valid_command_name2(Val).
+
+is_valid_command_name2(<<>>) ->
+ true;
+is_valid_command_name2(<<K:8, Rest/binary>>) when K >= $a andalso K =< $z orelse K == $_ ->
+ is_valid_command_name2(Rest);
+is_valid_command_name2(_) ->
+ false.
+
+key_split(Args, Fields) ->
+ {_, Order1, Results1, Required1} = lists:foldl(
+ fun({Field, Default}, {Idx, Order, Results, Required}) ->
+ {Idx + 1, Order#{Field => Idx}, [Default | Results], Required};
+ (Field, {Idx, Order, Results, Required}) ->
+ {Idx + 1, Order#{Field => Idx}, [none | Results], Required#{Field => 1}}
+ end, {1, #{}, [], #{}}, Fields),
+ key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}).
+
+key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 ->
+ parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]);
+key_split([], Results, _Order, _Required, _Duplicates) ->
+ Results;
+key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) ->
+ case maps:find(Arg, Order) of
+ {ok, Idx} ->
+ case maps:is_key(Arg, Duplicates) of
+ false ->
+ Results2 = setelement(Idx, Results, Value),
+ key_split(Rest, Results2, Order, maps:remove(Arg, Required), Duplicates#{Arg => 1});
+ true ->
+ parse_error(<<"Duplicate field '~s'">>, [Arg])
+ end;
+ _ ->
+ parse_error(<<"Unknown field '~s'">>, [Arg])
+ end.
+
+report_error(Format, Args) ->
+ throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}).
+
+parse_error(Format, Args) ->
+ {error, iolist_to_binary(io_lib:format(Format, Args))}.
+
+opt_type(api_permissions) ->
+ fun parse_api_permissions/1;
+opt_type(_) ->
+ [api_permissions].
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index ae2fac3e1..8622ea8d0 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -403,7 +403,8 @@ registered_vhosts() ->
reload_config() ->
ejabberd_config:reload_file(),
acl:start(),
- shaper:start().
+ shaper:start(),
+ ejabberd_access_permissions:invalidate().
%%%
%%% Cluster management
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index 33da45013..e4087142b 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -51,6 +51,7 @@ start(normal, _Args) ->
db_init(),
start(),
translate:start(),
+ ejabberd_access_permissions:start_link(),
ejabberd_ctl:init(),
ejabberd_commands:init(),
ejabberd_admin:start(),
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index d5649b2d7..173071d6f 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -218,23 +218,26 @@
get_command_format/1,
get_command_format/2,
get_command_format/3,
- get_command_policy_and_scope/1,
+ 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,
+ get_exposed_commands/0,
register_commands/1,
- unregister_commands/1,
- expose_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
- ]).
+ opt_type/1,
+ get_commands_spec/0,
+ get_commands_definition/0,
+ get_commands_definition/1,
+ execute_command2/3,
+ execute_command2/4]).
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
@@ -280,7 +283,8 @@ init() ->
{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()),
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0).
-spec register_commands([ejabberd_commands()]) -> ok.
@@ -296,7 +300,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,7 +312,9 @@ unregister_commands(Commands) ->
fun(Command) ->
mnesia:dirty_delete_object(Command)
end,
- Commands).
+ Commands),
+ ejabberd_access_permissions:invalidate(),
+ ok.
%% @doc Expose command through ejabberd ReST API.
%% Pass a list of command names or policy to expose.
@@ -427,6 +435,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,6 +459,18 @@ get_commands_definition(Version) ->
end,
lists:foldl(F, [], L).
+execute_command2(Name, Arguments, CallerInfo) ->
+ execute_command(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
+
+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.
+
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
@@ -811,6 +834,8 @@ is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
false;
+is_admin(_Name, Map, _extra) when is_map(Map) ->
+ true;
is_admin(Name, Auth, Extra) ->
{ACLInfo, Server} = case Auth of
{U, S, _, _} ->
@@ -832,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
deny -> false
end.
+permission_addon() ->
+ [{<<"'commands' option compatibility shim">>,
+ {[],
+ [{access, ejabberd_config:get_option(commands_admin_access,
+ fun(V) -> V end,
+ none)}],
+ {get_exposed_commands(), []}}}].
+
opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
opt_type(commands) ->
fun(V) when is_list(V) -> V end;
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index 6ca6a40a8..517f2fd2f 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -178,7 +178,8 @@ read_file(File, Opts) ->
-spec load_file(string()) -> ok.
load_file(File) ->
- State = read_file(File),
+ State0 = read_file(File),
+ State = validate_opts(State0),
set_opts(State).
-spec reload_file() -> ok.
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index d52b55cf9..a96a28016 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
- Result = ejabberd_commands:execute_command(AccessCommands,
- Auth, Command,
- ArgsFormatted,
- Version),
+ CI = case Auth of
+ {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
+ _ -> #{}
+ end,
+ CI2 = CI#{caller_module => ?MODULE},
+ Result = ejabberd_commands:execute_command2(Command,
+ ArgsFormatted,
+ CI2,
+ Version),
format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} =
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 4541190ad..d11548c22 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -42,8 +42,10 @@
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
+ check_token/1,
check_token/4,
check_token/2,
+ scope_in_scope_list/2,
process/2,
opt_type/1]).
@@ -305,6 +307,29 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
+scope_in_scope_list(Scope, ScopeList) ->
+ TokenScopeSet = oauth2_priv_set:new(Scope),
+ lists:any(fun(Scope2) ->
+ oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
+ ScopeList).
+
+check_token(Token) ->
+ case lookup(Token) of
+ {ok, #oauth_token{us = US,
+ scope = TokenScope,
+ expire = Expire}} ->
+ {MegaSecs, Secs, _} = os:timestamp(),
+ TS = 1000000 * MegaSecs + Secs,
+ if
+ Expire > TS ->
+ {ok, US, TokenScope};
+ true ->
+ {false, expired}
+ end;
+ _ ->
+ {false, not_found}
+ end.
+
check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 7a95f8c6f..491383769 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -118,9 +118,11 @@
%% -------------------
start(_Host, _Opts) ->
+ ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
ok.
stop(_Host) ->
+ ejabberd_access_permissions:unregister_permission_addon(?MODULE),
ok.
depends(_Host, _Opts) ->
@@ -130,76 +132,39 @@ depends(_Host, _Opts) ->
%% basic auth
%% ----------
-check_permissions(Request, Command) ->
- case catch binary_to_existing_atom(Command, utf8) of
- Call when is_atom(Call) ->
- {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
- check_permissions2(Request, Call, CommandPolicy, Scope);
- _ ->
- json_error(404, 40, <<"Endpoint not found.">>)
- end.
-
-check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
- when HTTPAuth /= undefined ->
- Admin =
- case lists:keysearch(<<"X-Admin">>, 1, Headers) of
- {value, {_, <<"true">>}} -> true;
- _ -> false
- end,
- Auth =
- case HTTPAuth of
- {SJID, Pass} ->
- case jid:from_string(SJID) of
- #jid{user = User, server = Server} ->
- case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
- true -> {ok, {User, Server, Pass, Admin}};
- false -> false
- end;
- _ ->
- false
- end;
- {oauth, Token, _} ->
- case oauth_check_token(ScopeList, Token) of
- {ok, user, {User, Server}} ->
- {ok, {User, Server, {oauth, Token}, Admin}};
- {false, Reason} ->
- {false, Reason}
- end;
- _ ->
- false
- end,
- case Auth of
- {ok, A} -> {allowed, Call, A};
- {false, no_matching_scope} -> outofscope_response();
- _ -> unauthorized_response()
+extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
+ Info = case HTTPAuth of
+ {SJID, Pass} ->
+ case jid:from_string(SJID) of
+ #jid{luser = User, lserver = Server} ->
+ case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
+ true ->
+ #{usr => {User, Server, <<"">>}, caller_server => Server};
+ false ->
+ {error, invalid_auth}
+ end;
+ _ ->
+ {error, invalid_auth}
+ end;
+ {oauth, Token, _} ->
+ case ejabberd_oauth:check_token(Token) of
+ {ok, {U, S}, Scope} ->
+ #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
+ {false, Reason} ->
+ {error, Reason}
+ end;
+ _ ->
+ #{}
+ end,
+ case Info of
+ Map when is_map(Map) ->
+ Map#{caller_module => ?MODULE, ip => IP};
+ _ ->
+ ?DEBUG("Invalid auth data: ~p", [Info]),
+ Info
end;
-check_permissions2(_Request, Call, open, _Scope) ->
- {allowed, Call, noauth};
-check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) ->
- Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
- fun(V) -> V end,
- none),
- Res = acl:match_rule(global, Access, IP),
- case Res of
- all ->
- {allowed, Call, admin};
- [all] ->
- {allowed, Call, admin};
- allow ->
- {allowed, Call, admin};
- Commands when is_list(Commands) ->
- case lists:member(Call, Commands) of
- true -> {allowed, Call, admin};
- _ -> outofscope_response()
- end;
- _E ->
- {allowed, Call, noauth}
- end;
-check_permissions2(_Request, _Call, _Policy, _Scope) ->
- unauthorized_response().
-
-oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
- ejabberd_oauth:check_token(ScopeList, Token).
+extract_auth(#request{ip = IP}) ->
+ #{ip => IP, caller_module => ?MODULE}.
%% ------------------
%% command processing
@@ -210,19 +175,12 @@ oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response(<<"Missing POST data">>);
-process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
+process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
Version = get_api_version(Req),
try
Args = extract_args(Data),
log(Call, Args, IPPort),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- Result = handle(Cmd, Auth, Args, Version, IP),
- json_format(Result);
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
+ perform_call(Call, Args, Req, Version)
catch
%% TODO We need to refactor to remove redundant error return formatting
throw:{error, unknown_command} ->
@@ -234,7 +192,7 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} =
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
+process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
Version = get_api_version(Req),
try
Args = case Data of
@@ -242,14 +200,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
_ -> Data
end,
log(Call, Args, IP),
- case check_permissions(Req, Call) of
- {allowed, Cmd, Auth} ->
- Result = handle(Cmd, Auth, Args, Version, IP),
- json_format(Result);
- %% Warning: check_permission direcly formats 401 reply if not authorized
- ErrorResponse ->
- ErrorResponse
- end
+ perform_call(Call, Args, Req, Version)
catch
%% TODO We need to refactor to remove redundant error return formatting
throw:{error, unknown_command} ->
@@ -267,6 +218,22 @@ process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
json_error(400, 40, <<"Missing command name.">>).
+perform_call(Command, Args, Req, Version) ->
+ case catch binary_to_existing_atom(Command, utf8) of
+ Call when is_atom(Call) ->
+ case extract_auth(Req) of
+ {error, expired} -> invalid_token_response();
+ {error, not_found} -> invalid_token_response();
+ {error, invalid_auth} -> unauthorized_response();
+ {error, _} -> unauthorized_response();
+ Auth when is_map(Auth) ->
+ Result = handle(Call, Auth, Args, Version),
+ json_format(Result)
+ end;
+ _ ->
+ json_error(404, 40, <<"Endpoint not found.">>)
+ end.
+
%% Be tolerant to make API more easily usable from command-line pipe.
extract_args(<<"\n">>) -> [];
extract_args(Data) ->
@@ -298,7 +265,7 @@ get_api_version([]) ->
%% 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) ->
+handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
@@ -315,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
try
- handle2(Call, Auth, match(Args2, Spec), Version, IP)
+ handle2(Call, Auth, match(Args2, Spec), Version)
catch throw:not_found ->
{404, <<"not_found">>};
throw:{not_found, Why} when is_atom(Why) ->
@@ -354,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{400, <<"Error">>}
end.
-handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
+handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
- ejabberd_command(Auth, Call, ArgsFormatted, Version, IP).
+ case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
+ {error, Error} ->
+ throw(Error);
+ Res ->
+ format_command_result(Call, Auth, Res, Version)
+ end.
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
@@ -456,18 +428,6 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
-ejabberd_command(Auth, Cmd, Args, Version, IP) ->
- Access = case Auth of
- admin -> [];
- _ -> undefined
- end,
- case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of
- {error, Error} ->
- throw(Error);
- Res ->
- format_command_result(Cmd, Auth, Res, Version)
- end.
-
format_command_result(Cmd, Auth, Result, Version) ->
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
@@ -538,6 +498,9 @@ format_error_result(_ErrorAtom, Code, Msg) ->
{500, Code, iolist_to_binary(Msg)}.
unauthorized_response() ->
+ json_error(401, 10, <<"You are not authorized to call this command.">>).
+
+invalid_token_response() ->
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
outofscope_response() ->
@@ -571,5 +534,31 @@ log(Call, Args, {Addr, Port}) ->
log(Call, Args, IP) ->
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
+permission_addon() ->
+ Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
+ fun(V) -> V end,
+ none),
+ Rules = acl:resolve_access(Access, global),
+ R = lists:filtermap(
+ fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow ->
+ {true, {[{allow, AclRules}], {[<<"*">>], []}}};
+ ({List, AclRules}) when is_list(List) ->
+ {true, {[{allow, AclRules}], {List, []}}};
+ (_) ->
+ false
+ end, Rules),
+ case R of
+ [] ->
+ none;
+ _ ->
+ {_, Res} = lists:foldl(
+ fun({R2, L2}, {Idx, Acc}) ->
+ {Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ",
+ (integer_to_binary(Idx))/binary>>,
+ {[?MODULE], [{access, R2}], L2}} | Acc]}
+ end, {1, []}, R),
+ Res
+ end.
+
mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1;
mod_opt_type(_) -> [admin_ip_access].