]> granicus.if.org Git - ejabberd/commitdiff
New api permissions framework
authorPaweł Chmielowski <pchmielowski@process-one.net>
Wed, 5 Oct 2016 11:21:11 +0000 (13:21 +0200)
committerPaweł Chmielowski <pchmielowski@process-one.net>
Wed, 5 Oct 2016 11:21:11 +0000 (13:21 +0200)
src/acl.erl
src/ejabberd_access_permissions.erl [new file with mode: 0644]
src/ejabberd_admin.erl
src/ejabberd_app.erl
src/ejabberd_commands.erl
src/ejabberd_config.erl
src/ejabberd_ctl.erl
src/ejabberd_oauth.erl
src/mod_http_api.erl

index 349198182a779fd8296db52875cb50c328673fe5..1476081dd8f841dbc28322b3f83f6019f84d8e69 100644 (file)
@@ -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 (file)
index 0000000..f37de9a
--- /dev/null
@@ -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].
index ae2fac3e1016446236a546ef67b3da9e9f616a80..8622ea8d0853e9e6174281c5cd77badc5ef1d436 100644 (file)
@@ -403,7 +403,8 @@ registered_vhosts() ->
 reload_config() ->
     ejabberd_config:reload_file(),
     acl:start(),
-    shaper:start().
+    shaper:start(),
+    ejabberd_access_permissions:invalidate().
 
 %%%
 %%% Cluster management
index 33da450135b0657a0e44cefcd3d722b757172597..e4087142b095c61bf5a5ba19f5f981fdcdd132ab 100644 (file)
@@ -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(),
index d5649b2d75aaabb983f0878d1c08cf608c5f75d4..173071d6fb868e4bb2080051b4caad7557374d58 100644 (file)
         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;
index 6ca6a40a846a03397aab0bb739ff30ca758c6945..517f2fd2fc0f10cf04de9554a038a38730e38b39 100644 (file)
@@ -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.
index d52b55cf9a212a00dd92e8df1920f05e97508f9d..a96a28016006fc3dd8bf520924dad8477d102bc7 100644 (file)
@@ -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} =
index 4541190adf636fa8efe6f4642351130573763985..d11548c22eca88ca92fc7bb44550e59fabeff714 100644 (file)
          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),
index 7a95f8c6f039e2f601f7e978624bd232db42b6df..4913837694b93986327dd054c28609e8fef77650 100644 (file)
 %% -------------------
 
 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].