diff options
Diffstat (limited to 'src/ejabberd_access_permissions.erl')
-rw-r--r-- | src/ejabberd_access_permissions.erl | 527 |
1 files changed, 527 insertions, 0 deletions
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]. |