mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
New api permissions framework
This commit is contained in:
parent
9cee3760db
commit
98e0123ca4
38
src/acl.erl
38
src/acl.erl
@ -36,7 +36,8 @@
|
|||||||
acl_rule_verify/1, access_matches/3,
|
acl_rule_verify/1, access_matches/3,
|
||||||
transform_access_rules_config/1,
|
transform_access_rules_config/1,
|
||||||
parse_ip_netmask/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("ejabberd.hrl").
|
||||||
-include("logger.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) ->
|
acl_rule_matches(_ACL, _Data, _Host) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
-spec access_matches(atom()|list(), any(), global|binary()) -> any().
|
resolve_access(all, _Host) ->
|
||||||
access_matches(all, _Data, _Host) ->
|
all;
|
||||||
allow;
|
resolve_access(none, _Host) ->
|
||||||
access_matches(none, _Data, _Host) ->
|
none;
|
||||||
deny;
|
resolve_access(Name, Host) when is_atom(Name) ->
|
||||||
access_matches(Name, Data, Host) when is_atom(Name) ->
|
|
||||||
GAccess = mnesia:dirty_read(access, {Name, global}),
|
GAccess = mnesia:dirty_read(access, {Name, global}),
|
||||||
LAccess =
|
LAccess =
|
||||||
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
|
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
|
||||||
true -> []
|
true -> []
|
||||||
end,
|
end,
|
||||||
case GAccess ++ LAccess of
|
case GAccess ++ LAccess of
|
||||||
[] ->
|
[] ->
|
||||||
deny;
|
[];
|
||||||
AccessList ->
|
AccessList ->
|
||||||
Rules = lists:flatmap(
|
lists:flatmap(
|
||||||
fun(#access{rules = Rs}) ->
|
fun(#access{rules = Rs}) ->
|
||||||
Rs
|
Rs
|
||||||
end, AccessList),
|
end, AccessList)
|
||||||
access_rules_matches(Rules, Data, Host)
|
|
||||||
end;
|
end;
|
||||||
access_matches(Rules, Data, Host) when is_list(Rules) ->
|
resolve_access(Rules, _Host) when is_list(Rules) ->
|
||||||
access_rules_matches(Rules, Data, Host).
|
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().
|
-spec access_rules_matches(list(), any(), global|binary()) -> any().
|
||||||
|
|
||||||
|
527
src/ejabberd_access_permissions.erl
Normal file
527
src/ejabberd_access_permissions.erl
Normal file
@ -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].
|
@ -403,7 +403,8 @@ registered_vhosts() ->
|
|||||||
reload_config() ->
|
reload_config() ->
|
||||||
ejabberd_config:reload_file(),
|
ejabberd_config:reload_file(),
|
||||||
acl:start(),
|
acl:start(),
|
||||||
shaper:start().
|
shaper:start(),
|
||||||
|
ejabberd_access_permissions:invalidate().
|
||||||
|
|
||||||
%%%
|
%%%
|
||||||
%%% Cluster management
|
%%% Cluster management
|
||||||
|
@ -51,6 +51,7 @@ start(normal, _Args) ->
|
|||||||
db_init(),
|
db_init(),
|
||||||
start(),
|
start(),
|
||||||
translate:start(),
|
translate:start(),
|
||||||
|
ejabberd_access_permissions:start_link(),
|
||||||
ejabberd_ctl:init(),
|
ejabberd_ctl:init(),
|
||||||
ejabberd_commands:init(),
|
ejabberd_commands:init(),
|
||||||
ejabberd_admin:start(),
|
ejabberd_admin:start(),
|
||||||
|
@ -218,23 +218,26 @@
|
|||||||
get_command_format/1,
|
get_command_format/1,
|
||||||
get_command_format/2,
|
get_command_format/2,
|
||||||
get_command_format/3,
|
get_command_format/3,
|
||||||
get_command_policy_and_scope/1,
|
get_command_policy_and_scope/1,
|
||||||
get_command_definition/1,
|
get_command_definition/1,
|
||||||
get_command_definition/2,
|
get_command_definition/2,
|
||||||
get_tags_commands/0,
|
get_tags_commands/0,
|
||||||
get_tags_commands/1,
|
get_tags_commands/1,
|
||||||
get_exposed_commands/0,
|
get_exposed_commands/0,
|
||||||
register_commands/1,
|
register_commands/1,
|
||||||
unregister_commands/1,
|
unregister_commands/1,
|
||||||
expose_commands/1,
|
expose_commands/1,
|
||||||
execute_command/2,
|
execute_command/2,
|
||||||
execute_command/3,
|
execute_command/3,
|
||||||
execute_command/4,
|
execute_command/4,
|
||||||
execute_command/5,
|
execute_command/5,
|
||||||
execute_command/6,
|
execute_command/6,
|
||||||
opt_type/1,
|
opt_type/1,
|
||||||
get_commands_spec/0
|
get_commands_spec/0,
|
||||||
]).
|
get_commands_definition/0,
|
||||||
|
get_commands_definition/1,
|
||||||
|
execute_command2/3,
|
||||||
|
execute_command2/4]).
|
||||||
|
|
||||||
-include("ejabberd_commands.hrl").
|
-include("ejabberd_commands.hrl").
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
@ -280,7 +283,8 @@ init() ->
|
|||||||
{attributes, record_info(fields, ejabberd_commands)},
|
{attributes, record_info(fields, ejabberd_commands)},
|
||||||
{type, bag}]),
|
{type, bag}]),
|
||||||
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
|
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.
|
-spec register_commands([ejabberd_commands()]) -> ok.
|
||||||
|
|
||||||
@ -296,7 +300,9 @@ register_commands(Commands) ->
|
|||||||
mnesia:dirty_write(Command)
|
mnesia:dirty_write(Command)
|
||||||
%% ?DEBUG("This command is already defined:~n~p", [Command])
|
%% ?DEBUG("This command is already defined:~n~p", [Command])
|
||||||
end,
|
end,
|
||||||
Commands).
|
Commands),
|
||||||
|
ejabberd_access_permissions:invalidate(),
|
||||||
|
ok.
|
||||||
|
|
||||||
-spec unregister_commands([ejabberd_commands()]) -> ok.
|
-spec unregister_commands([ejabberd_commands()]) -> ok.
|
||||||
|
|
||||||
@ -306,7 +312,9 @@ unregister_commands(Commands) ->
|
|||||||
fun(Command) ->
|
fun(Command) ->
|
||||||
mnesia:dirty_delete_object(Command)
|
mnesia:dirty_delete_object(Command)
|
||||||
end,
|
end,
|
||||||
Commands).
|
Commands),
|
||||||
|
ejabberd_access_permissions:invalidate(),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% @doc Expose command through ejabberd ReST API.
|
%% @doc Expose command through ejabberd ReST API.
|
||||||
%% Pass a list of command names or policy to expose.
|
%% Pass a list of command names or policy to expose.
|
||||||
@ -427,6 +435,9 @@ get_command_definition(Name, Version) ->
|
|||||||
_E -> throw({error, unknown_command})
|
_E -> throw({error, unknown_command})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
get_commands_definition() ->
|
||||||
|
get_commands_definition(?DEFAULT_VERSION).
|
||||||
|
|
||||||
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
||||||
|
|
||||||
% @doc Returns all commands for a given API version
|
% @doc Returns all commands for a given API version
|
||||||
@ -448,6 +459,18 @@ get_commands_definition(Version) ->
|
|||||||
end,
|
end,
|
||||||
lists:foldl(F, [], L).
|
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
|
%% @spec (Name::atom(), Arguments) -> ResultTerm
|
||||||
%% where
|
%% where
|
||||||
%% Arguments = [any()]
|
%% Arguments = [any()]
|
||||||
@ -811,6 +834,8 @@ is_admin(_Name, admin, _Extra) ->
|
|||||||
true;
|
true;
|
||||||
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
|
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
|
||||||
false;
|
false;
|
||||||
|
is_admin(_Name, Map, _extra) when is_map(Map) ->
|
||||||
|
true;
|
||||||
is_admin(Name, Auth, Extra) ->
|
is_admin(Name, Auth, Extra) ->
|
||||||
{ACLInfo, Server} = case Auth of
|
{ACLInfo, Server} = case Auth of
|
||||||
{U, S, _, _} ->
|
{U, S, _, _} ->
|
||||||
@ -832,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
|
|||||||
deny -> false
|
deny -> false
|
||||||
end.
|
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_admin_access) -> fun acl:access_rules_validator/1;
|
||||||
opt_type(commands) ->
|
opt_type(commands) ->
|
||||||
fun(V) when is_list(V) -> V end;
|
fun(V) when is_list(V) -> V end;
|
||||||
|
@ -178,7 +178,8 @@ read_file(File, Opts) ->
|
|||||||
-spec load_file(string()) -> ok.
|
-spec load_file(string()) -> ok.
|
||||||
|
|
||||||
load_file(File) ->
|
load_file(File) ->
|
||||||
State = read_file(File),
|
State0 = read_file(File),
|
||||||
|
State = validate_opts(State0),
|
||||||
set_opts(State).
|
set_opts(State).
|
||||||
|
|
||||||
-spec reload_file() -> ok.
|
-spec reload_file() -> ok.
|
||||||
|
@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
|
|||||||
{ArgsFormat, ResultFormat} ->
|
{ArgsFormat, ResultFormat} ->
|
||||||
case (catch format_args(Args, ArgsFormat)) of
|
case (catch format_args(Args, ArgsFormat)) of
|
||||||
ArgsFormatted when is_list(ArgsFormatted) ->
|
ArgsFormatted when is_list(ArgsFormatted) ->
|
||||||
Result = ejabberd_commands:execute_command(AccessCommands,
|
CI = case Auth of
|
||||||
Auth, Command,
|
{U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
|
||||||
ArgsFormatted,
|
_ -> #{}
|
||||||
Version),
|
end,
|
||||||
|
CI2 = CI#{caller_module => ?MODULE},
|
||||||
|
Result = ejabberd_commands:execute_command2(Command,
|
||||||
|
ArgsFormatted,
|
||||||
|
CI2,
|
||||||
|
Version),
|
||||||
format_result(Result, ResultFormat);
|
format_result(Result, ResultFormat);
|
||||||
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
||||||
{NumCompa, TextCompa} =
|
{NumCompa, TextCompa} =
|
||||||
|
@ -42,8 +42,10 @@
|
|||||||
associate_access_code/3,
|
associate_access_code/3,
|
||||||
associate_access_token/3,
|
associate_access_token/3,
|
||||||
associate_refresh_token/3,
|
associate_refresh_token/3,
|
||||||
|
check_token/1,
|
||||||
check_token/4,
|
check_token/4,
|
||||||
check_token/2,
|
check_token/2,
|
||||||
|
scope_in_scope_list/2,
|
||||||
process/2,
|
process/2,
|
||||||
opt_type/1]).
|
opt_type/1]).
|
||||||
|
|
||||||
@ -305,6 +307,29 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) ->
|
|||||||
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
|
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
|
||||||
{ok, AppContext}.
|
{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) ->
|
check_token(User, Server, ScopeList, Token) ->
|
||||||
LUser = jid:nodeprep(User),
|
LUser = jid:nodeprep(User),
|
||||||
LServer = jid:nameprep(Server),
|
LServer = jid:nameprep(Server),
|
||||||
|
@ -118,9 +118,11 @@
|
|||||||
%% -------------------
|
%% -------------------
|
||||||
|
|
||||||
start(_Host, _Opts) ->
|
start(_Host, _Opts) ->
|
||||||
|
ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
stop(_Host) ->
|
stop(_Host) ->
|
||||||
|
ejabberd_access_permissions:unregister_permission_addon(?MODULE),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
depends(_Host, _Opts) ->
|
depends(_Host, _Opts) ->
|
||||||
@ -130,76 +132,39 @@ depends(_Host, _Opts) ->
|
|||||||
%% basic auth
|
%% basic auth
|
||||||
%% ----------
|
%% ----------
|
||||||
|
|
||||||
check_permissions(Request, Command) ->
|
extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
|
||||||
case catch binary_to_existing_atom(Command, utf8) of
|
Info = case HTTPAuth of
|
||||||
Call when is_atom(Call) ->
|
{SJID, Pass} ->
|
||||||
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
|
case jid:from_string(SJID) of
|
||||||
check_permissions2(Request, Call, CommandPolicy, Scope);
|
#jid{luser = User, lserver = Server} ->
|
||||||
_ ->
|
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
||||||
json_error(404, 40, <<"Endpoint not found.">>)
|
true ->
|
||||||
end.
|
#{usr => {User, Server, <<"">>}, caller_server => Server};
|
||||||
|
false ->
|
||||||
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
|
{error, invalid_auth}
|
||||||
when HTTPAuth /= undefined ->
|
end;
|
||||||
Admin =
|
_ ->
|
||||||
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
|
{error, invalid_auth}
|
||||||
{value, {_, <<"true">>}} -> true;
|
end;
|
||||||
_ -> false
|
{oauth, Token, _} ->
|
||||||
end,
|
case ejabberd_oauth:check_token(Token) of
|
||||||
Auth =
|
{ok, {U, S}, Scope} ->
|
||||||
case HTTPAuth of
|
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
|
||||||
{SJID, Pass} ->
|
{false, Reason} ->
|
||||||
case jid:from_string(SJID) of
|
{error, Reason}
|
||||||
#jid{user = User, server = Server} ->
|
end;
|
||||||
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
_ ->
|
||||||
true -> {ok, {User, Server, Pass, Admin}};
|
#{}
|
||||||
false -> false
|
end,
|
||||||
end;
|
case Info of
|
||||||
_ ->
|
Map when is_map(Map) ->
|
||||||
false
|
Map#{caller_module => ?MODULE, ip => IP};
|
||||||
end;
|
_ ->
|
||||||
{oauth, Token, _} ->
|
?DEBUG("Invalid auth data: ~p", [Info]),
|
||||||
case oauth_check_token(ScopeList, Token) of
|
Info
|
||||||
{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()
|
|
||||||
end;
|
end;
|
||||||
check_permissions2(_Request, Call, open, _Scope) ->
|
extract_auth(#request{ip = IP}) ->
|
||||||
{allowed, Call, noauth};
|
#{ip => IP, caller_module => ?MODULE}.
|
||||||
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).
|
|
||||||
|
|
||||||
%% ------------------
|
%% ------------------
|
||||||
%% command processing
|
%% command processing
|
||||||
@ -210,19 +175,12 @@ oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
|
|||||||
process(_, #request{method = 'POST', data = <<>>}) ->
|
process(_, #request{method = 'POST', data = <<>>}) ->
|
||||||
?DEBUG("Bad Request: no data", []),
|
?DEBUG("Bad Request: no data", []),
|
||||||
badrequest_response(<<"Missing POST 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),
|
Version = get_api_version(Req),
|
||||||
try
|
try
|
||||||
Args = extract_args(Data),
|
Args = extract_args(Data),
|
||||||
log(Call, Args, IPPort),
|
log(Call, Args, IPPort),
|
||||||
case check_permissions(Req, Call) of
|
perform_call(Call, Args, Req, Version)
|
||||||
{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
|
|
||||||
catch
|
catch
|
||||||
%% TODO We need to refactor to remove redundant error return formatting
|
%% TODO We need to refactor to remove redundant error return formatting
|
||||||
throw:{error, unknown_command} ->
|
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()]),
|
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
||||||
badrequest_response()
|
badrequest_response()
|
||||||
end;
|
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),
|
Version = get_api_version(Req),
|
||||||
try
|
try
|
||||||
Args = case Data of
|
Args = case Data of
|
||||||
@ -242,14 +200,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
|||||||
_ -> Data
|
_ -> Data
|
||||||
end,
|
end,
|
||||||
log(Call, Args, IP),
|
log(Call, Args, IP),
|
||||||
case check_permissions(Req, Call) of
|
perform_call(Call, Args, Req, Version)
|
||||||
{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
|
|
||||||
catch
|
catch
|
||||||
%% TODO We need to refactor to remove redundant error return formatting
|
%% TODO We need to refactor to remove redundant error return formatting
|
||||||
throw:{error, unknown_command} ->
|
throw:{error, unknown_command} ->
|
||||||
@ -267,6 +218,22 @@ process(_Path, Request) ->
|
|||||||
?DEBUG("Bad Request: no handler ~p", [Request]),
|
?DEBUG("Bad Request: no handler ~p", [Request]),
|
||||||
json_error(400, 40, <<"Missing command name.">>).
|
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.
|
%% Be tolerant to make API more easily usable from command-line pipe.
|
||||||
extract_args(<<"\n">>) -> [];
|
extract_args(<<"\n">>) -> [];
|
||||||
extract_args(Data) ->
|
extract_args(Data) ->
|
||||||
@ -298,7 +265,7 @@ get_api_version([]) ->
|
|||||||
%% TODO Check accept types of request before decided format of reply.
|
%% TODO Check accept types of request before decided format of reply.
|
||||||
|
|
||||||
% generic ejabberd command handler
|
% 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
|
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
||||||
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
||||||
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
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]
|
[{Key, undefined}|Acc]
|
||||||
end, [], ArgsSpec),
|
end, [], ArgsSpec),
|
||||||
try
|
try
|
||||||
handle2(Call, Auth, match(Args2, Spec), Version, IP)
|
handle2(Call, Auth, match(Args2, Spec), Version)
|
||||||
catch throw:not_found ->
|
catch throw:not_found ->
|
||||||
{404, <<"not_found">>};
|
{404, <<"not_found">>};
|
||||||
throw:{not_found, Why} when is_atom(Why) ->
|
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">>}
|
{400, <<"Error">>}
|
||||||
end.
|
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),
|
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
|
||||||
ArgsFormatted = format_args(Args, ArgsF),
|
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) ->
|
get_elem_delete(A, L) ->
|
||||||
case proplists:get_all_values(A, L) of
|
case proplists:get_all_values(A, L) of
|
||||||
@ -456,18 +428,6 @@ process_unicode_codepoints(Str) ->
|
|||||||
match(Args, Spec) ->
|
match(Args, Spec) ->
|
||||||
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- 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) ->
|
format_command_result(Cmd, Auth, Result, Version) ->
|
||||||
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
|
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
|
||||||
case {ResultFormat, Result} of
|
case {ResultFormat, Result} of
|
||||||
@ -538,6 +498,9 @@ format_error_result(_ErrorAtom, Code, Msg) ->
|
|||||||
{500, Code, iolist_to_binary(Msg)}.
|
{500, Code, iolist_to_binary(Msg)}.
|
||||||
|
|
||||||
unauthorized_response() ->
|
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.">>).
|
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
|
||||||
|
|
||||||
outofscope_response() ->
|
outofscope_response() ->
|
||||||
@ -571,5 +534,31 @@ log(Call, Args, {Addr, Port}) ->
|
|||||||
log(Call, Args, IP) ->
|
log(Call, Args, IP) ->
|
||||||
?INFO_MSG("API call ~s ~p (~p)", [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) -> fun acl:access_rules_validator/1;
|
||||||
mod_opt_type(_) -> [admin_ip_access].
|
mod_opt_type(_) -> [admin_ip_access].
|
||||||
|
Loading…
Reference in New Issue
Block a user