aboutsummaryrefslogtreecommitdiff
path: root/src/ejabberd_access_permissions.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/ejabberd_access_permissions.erl')
-rw-r--r--src/ejabberd_access_permissions.erl510
1 files changed, 166 insertions, 344 deletions
diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl
index 0c53795b8..2f63cb576 100644
--- a/src/ejabberd_access_permissions.erl
+++ b/src/ejabberd_access_permissions.erl
@@ -29,17 +29,13 @@
-include("logger.hrl").
-behaviour(gen_server).
--behaviour(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]).
+ validator/0,
+ show_current_definitions/0]).
%% gen_server callbacks
-export([init/1,
@@ -51,16 +47,29 @@
-define(SERVER, ?MODULE).
--record(state, {
- definitions = none,
- fragments_generators = []
-}).
+-record(state,
+ {definitions = none :: none | [definition()]}).
+
+-type state() :: #state{}.
+-type rule() :: {access, acl:access()} |
+ {acl, all | none | acl:acl_rule()}.
+-type what() :: all | none | [atom() | {tag, atom()}].
+-type who() :: rule() | {oauth, {[binary()], [rule()]}}.
+-type from() :: atom().
+-type permission() :: {binary(), {[from()], [who()], {what(), what()}}}.
+-type definition() :: {binary(), {[from()], [who()], [atom()] | all}}.
+-type caller_info() :: #{caller_module => module(),
+ caller_host => global | binary(),
+ tag => binary() | none,
+ extra_permissions => [definition()],
+ atom() => term()}.
+
+-export_type([permission/0]).
%%%===================================================================
%%% API
%%%===================================================================
-
--spec can_access(atom(), map()) -> allow | deny.
+-spec can_access(atom(), caller_info()) -> allow | deny.
can_access(Cmd, CallerInfo) ->
gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
@@ -68,65 +77,24 @@ can_access(Cmd, CallerInfo) ->
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().
+-spec show_current_definitions() -> [definition()].
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.
+-spec init([]) -> {ok, state()}.
init([]) ->
ejabberd_hooks:add(config_reloaded, ?MODULE, invalidate, 90),
{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{}}.
+-spec handle_call({can_access, atom(), caller_info()} |
+ show_current_definitions | term(),
+ term(), state()) -> {reply, term(), state()}.
handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
CallerModule = maps:get(caller_module, CallerInfo, none),
Host = maps:get(caller_host, CallerInfo, global),
@@ -134,123 +102,61 @@ handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
{State2, Defs0} = get_definitions(State),
Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0,
Res = lists:foldl(
- fun({Name, _} = Def, none) ->
- case matches_definition(Def, Cmd, CallerModule, Tag, 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),
+ fun({Name, _} = Def, none) ->
+ case matches_definition(Def, Cmd, CallerModule, Tag, 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]),
+ ?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{}}.
+-spec handle_cast(invalidate | term(), state()) -> {noreply, 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) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, invalidate, 90).
-%%--------------------------------------------------------------------
-%% @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
%%%===================================================================
-
--spec get_definitions(#state{}) -> {#state{}, any()}.
+-spec get_definitions(state()) -> {state(), [definition()]}.
get_definitions(#state{definitions = Defs} = State) when Defs /= none ->
{State, Defs};
-get_definitions(#state{definitions = none, fragments_generators = Gens} = State) ->
- DefaultOptions = [{<<"admin access">>,
- {[],
- [{acl,{acl,admin}},
- {oauth,[<<"ejabberd:admin">>],[{acl,{acl,admin}}]}],
- {all, [start, stop]}}}],
- ApiPerms = ejabberd_config:get_option(api_permissions, DefaultOptions),
+get_definitions(#state{definitions = none} = State) ->
+ ApiPerms = ejabberd_option:api_permissions(),
AllCommands = ejabberd_commands:get_commands_definition(),
- Frags = lists:foldl(
- fun({_Name, Generator}, Acc) ->
- Acc ++ Generator()
- end, [], Gens),
NDefs0 = lists:map(
fun({Name, {From, Who, {Add, Del}}}) ->
Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
{Name, {From, Who, Cmds}}
- end, ApiPerms ++ Frags),
+ end, ApiPerms),
NDefs = case lists:keyfind(<<"console commands">>, 1, NDefs0) of
false ->
[{<<"console commands">>,
@@ -262,6 +168,8 @@ get_definitions(#state{definitions = none, fragments_generators = Gens} = State)
end,
{State#state{definitions = NDefs}, NDefs}.
+-spec matches_definition(definition(), atom(), module(),
+ atom(), global | binary(), caller_info()) -> boolean().
matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInfo) ->
case What == all orelse lists:member(Cmd, What) of
true ->
@@ -271,25 +179,29 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInf
true ->
Scope = maps:get(oauth_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, Scopes, List}) when Scope /= none ->
- case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
- true ->
- lists:any(
- fun({access, Access}) ->
- acl:access_matches(Access, CallerInfo, Host) == allow;
- ({acl, Acl}) ->
- acl:acl_rule_matches(Acl, CallerInfo, Host)
- end, List);
- _ ->
- false
- end;
- (_) ->
- false
- end, Who);
+ fun({access, Access}) when Scope == none ->
+ acl:match_rule(Host, Access, CallerInfo) == allow;
+ ({acl, Name} = Acl) when Scope == none, is_atom(Name) ->
+ acl:match_acl(Host, Acl, CallerInfo);
+ ({acl, Acl}) when Scope == none ->
+ acl:match_acl(Host, Acl, CallerInfo);
+ ({oauth, {Scopes, List}}) when Scope /= none ->
+ case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
+ true ->
+ lists:any(
+ fun({access, Access}) ->
+ acl:match_rule(Host, Access, CallerInfo) == allow;
+ ({acl, Name} = Acl) when is_atom(Name) ->
+ acl:match_acl(Host, Acl, CallerInfo);
+ ({acl, Acl}) ->
+ acl:match_acl(Host, Acl, CallerInfo)
+ end, List);
+ _ ->
+ false
+ end;
+ (_) ->
+ false
+ end, Who);
_ ->
false
end;
@@ -297,12 +209,15 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInf
false
end.
+-spec filter_commands_with_permissions([#ejabberd_commands{}], what(), what()) -> [atom()].
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).
+-spec filter_commands_with_patterns([#ejabberd_commands{}], what(),
+ [#ejabberd_commands{}]) -> [#ejabberd_commands{}].
filter_commands_with_patterns([], _Patterns, Acc) ->
Acc;
filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
@@ -313,6 +228,7 @@ filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
filter_commands_with_patterns(CRest, Patterns, Acc)
end.
+-spec command_matches_patterns(#ejabberd_commands{}, what()) -> boolean().
command_matches_patterns(_, all) ->
true;
command_matches_patterns(_, none) ->
@@ -332,125 +248,26 @@ command_matches_patterns(C, [_ | Tail]) ->
command_matches_patterns(C, Tail).
%%%===================================================================
-%%% Options parsing code
+%%% Validators
%%%===================================================================
-
-parse_api_permissions(Data) when is_list(Data) ->
- [parse_api_permission(Name, Args) || {Name, Args} <- Data].
-
-parse_api_permission(Name, Args0) ->
- Args = lists:flatten(Args0),
- {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:map(
- fun(Module) when is_atom(Module) ->
- Module;
- ([{tag, Tag}]) when is_binary(Tag) ->
- {tag, Tag};
- (Val) ->
- report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
- [Val, Name])
- end, 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([Val]) ->
- [NVal] = parse_who(Name, [Val], ParseOauth),
- NVal;
- ({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]);
- 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 ->
- Nested = parse_who(Name, lists:flatten(OauthList), scope),
- {Scopes, Rest} = lists:partition(
- fun({scope, _}) -> true;
- (_) -> false
- end, Nested),
- case Scopes of
- [] ->
- report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>,
- [Name]);
- _ ->
- {oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest}
- end;
- scope ->
- report_error(<<"Oauth rule can't be embedded 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, {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
- 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),
+-spec parse_what([binary()]) -> {what(), what()}.
+parse_what(Defs) ->
+ {A, D} =
+ lists:foldl(
+ fun(Def, {Add, Del}) ->
+ case parse_single_what(Def) of
+ {error, Err} ->
+ econf:fail({invalid_syntax, [Err, ": ", Def]});
+ 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};
@@ -458,11 +275,9 @@ parse_what(Name, Defs) when is_list(Defs) ->
{A2, none};
V ->
V
- end;
-parse_what(Name, Val) ->
- report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
- [Val, Name]).
+ end.
+-spec parse_single_what(binary()) -> atom() | {neg, atom()} | {tag, atom()} | {error, string()}.
parse_single_what(<<"*">>) ->
all;
parse_single_what(<<"!*">>) ->
@@ -470,7 +285,7 @@ parse_single_what(<<"!*">>) ->
parse_single_what(<<"!", Rest/binary>>) ->
case parse_single_what(Rest) of
{neg, _} ->
- {error, <<"Double negation">>};
+ {error, "double negation"};
{error, _} = Err ->
Err;
V ->
@@ -485,71 +300,78 @@ parse_single_what(<<"[tag:", Rest/binary>>) ->
V when is_atom(V) ->
{tag, V};
_ ->
- {error, <<"Invalid tag">>}
+ {error, "invalid tag"}
end;
_ ->
- {error, <<"Invalid tag">>}
+ {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(Atom) when is_atom(Atom) ->
- parse_single_what(atom_to_binary(Atom, latin1));
-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 >= $0 andalso K =< $9)
- orelse K == $_ 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, maps:put(Field, Idx, Order), [Default | Results], Required};
- (Field, {Idx, Order, Results, Required}) ->
- {Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)}
- 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), maps:put(Arg, 1, Duplicates));
- true ->
- parse_error(<<"Duplicate field '~s'">>, [Arg])
- end;
- _ ->
- parse_error(<<"Unknown field '~s'">>, [Arg])
+parse_single_what(B) ->
+ case re:run(B, "^[a-z0-9_\\-]*$") of
+ nomatch -> {error, "invalid command"};
+ _ -> binary_to_atom(B, latin1)
end.
-report_error(Format, Args) ->
- throw({invalid_syntax, (str:format(Format, Args))}).
-
-parse_error(Format, Args) ->
- {error, (str:format(Format, Args))}.
-
-opt_type(api_permissions) ->
- fun parse_api_permissions/1;
-opt_type(_) ->
- [api_permissions].
+validator(Map, Opts) ->
+ econf:and_then(
+ fun(L) when is_list(L) ->
+ lists:map(
+ fun({K, V}) -> {(econf:atom())(K), V};
+ (A) -> {acl, (econf:atom())(A)}
+ end, lists:flatten(L));
+ (A) ->
+ [{acl, (econf:atom())(A)}]
+ end,
+ econf:and_then(
+ econf:options(maps:merge(acl:validators(), Map), Opts),
+ fun(Rules) ->
+ lists:flatmap(
+ fun({Type, Rs}) when is_list(Rs) ->
+ case maps:is_key(Type, acl:validators()) of
+ true -> [{acl, {Type, R}} || R <- Rs];
+ false -> [{Type, Rs}]
+ end;
+ (Other) ->
+ [Other]
+ end, Rules)
+ end)).
+
+validator(from) ->
+ fun(L) when is_list(L) ->
+ lists:map(
+ fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)};
+ (A) -> (econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A)
+ end, lists:flatten(L));
+ (A) ->
+ [(econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A)]
+ end;
+validator(what) ->
+ econf:and_then(
+ econf:list_or_single(econf:non_empty(econf:binary())),
+ fun parse_what/1);
+validator(who) ->
+ validator(#{access => econf:acl(), oauth => validator(oauth)}, []);
+validator(oauth) ->
+ econf:and_then(
+ validator(#{access => econf:acl(),
+ scope => econf:non_empty(
+ econf:list_or_single(econf:binary()))},
+ [{required, [scope]}]),
+ fun(Os) ->
+ {[Scopes], Rest} = proplists:split(Os, [scope]),
+ {lists:flatten([S || {_, S} <- Scopes]), Rest}
+ end).
+
+validator() ->
+ econf:map(
+ econf:binary(),
+ econf:and_then(
+ econf:options(
+ #{from => validator(from),
+ what => validator(what),
+ who => validator(who)}),
+ fun(Os) ->
+ {proplists:get_value(from, Os, []),
+ proplists:get_value(who, Os, none),
+ proplists:get_value(what, Os, [])}
+ end),
+ [unique]).