aboutsummaryrefslogtreecommitdiff
path: root/src/acl.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/acl.erl')
-rw-r--r--src/acl.erl985
1 files changed, 324 insertions, 661 deletions
diff --git a/src/acl.erl b/src/acl.erl
index 897996976..cf6861a6f 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -1,11 +1,5 @@
%%%----------------------------------------------------------------------
-%%% File : acl.erl
-%%% Author : Alexey Shchepin <alexey@process-one.net>
-%%% Purpose : ACL support
-%%% Created : 18 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
-%%%
-%%%
-%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%% ejabberd, Copyright (C) 2002-2019 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
@@ -22,681 +16,350 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-
-module(acl).
+-behaviour(gen_server).
+
+-export([start_link/0]).
+-export([reload_from_config/0]).
+-export([match_rule/3, match_acl/3]).
+-export([match_rules/4, match_acls/3]).
+-export([access_rules_validator/0, access_validator/0]).
+-export([validator/1, validators/0]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
--behaviour(ejabberd_config).
-
--author('alexey@process-one.net').
-
--export([add_access/3, clear/0]).
--export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
- load_from_config/0, match_rule/3, any_rules_allowed/3,
- transform_options/1, opt_type/1, acl_rule_matches/3,
- acl_rule_verify/1, access_matches/3,
- transform_access_rules_config/1,
- parse_ip_netmask/1,
- access_rules_validator/1, shaper_rules_validator/1]).
-
--include("ejabberd.hrl").
-include("logger.hrl").
--include("jlib.hrl").
-
--record(acl, {aclname, aclspec}).
--record(access, {name :: aclname(),
- rules = [] :: [access_rule()]}).
-
--type regexp() :: binary().
--type iprange() :: {inet:ip_address(), integer()} | binary().
--type glob() :: binary().
--type access_name() :: atom().
--type access_rule() :: {atom(), any()}.
--type host() :: binary().
--type aclname() :: {atom(), binary() | global}.
--type aclspec() :: all | none |
- {user, {binary(), host()} | binary()} |
- {server, binary()} |
- {resource, binary()} |
- {user_regexp, {regexp(), host()} | regexp()} |
- {shared_group, {binary(), host()} | binary()} |
- {user_regexp, {regexp(), host()} | regexp()} |
- {server_regexp, regexp()} |
- {resource_regexp, regexp()} |
- {node_regexp, {regexp(), regexp()}} |
- {user_glob, {glob(), host()} | glob()} |
- {server_glob, glob()} |
- {resource_glob, glob()} |
- {ip, iprange()} |
- {node_glob, {glob(), glob()}}.
-
--type acl() :: #acl{aclname :: aclname(),
- aclspec :: aclspec()}.
-
--export_type([acl/0]).
-
-start() ->
- case catch mnesia:table_info(acl, storage_type) of
- disc_copies ->
- mnesia:delete_table(acl);
- _ ->
- ok
- end,
- mnesia:create_table(acl,
- [{ram_copies, [node()]}, {type, bag},
- {local_content, true},
- {attributes, record_info(fields, acl)}]),
- mnesia:create_table(access,
- [{ram_copies, [node()]},
- {local_content, true},
- {attributes, record_info(fields, access)}]),
- mnesia:add_table_copy(acl, node(), ram_copies),
- mnesia:add_table_copy(access, node(), ram_copies),
- load_from_config(),
- ok.
-
--spec add(binary(), aclname(), aclspec()) -> ok | {error, any()}.
-
-add(Host, ACLName, ACLSpec) ->
- {ResL, BadNodes} = ejabberd_cluster:multicall(
- ?MODULE, add_local,
- [Host, ACLName, ACLSpec]),
- case lists:keyfind(aborted, 1, ResL) of
- false when BadNodes == [] ->
- ok;
- false ->
- {error, {failed_nodes, BadNodes}};
- Err ->
- {error, Err}
- end.
-
-add_local(Host, ACLName, ACLSpec) ->
- F = fun () ->
- mnesia:write(#acl{aclname = {ACLName, Host},
- aclspec = normalize_spec(ACLSpec)})
- end,
- case mnesia:transaction(F) of
- {atomic, ok} ->
- ok;
- Err ->
- Err
- end.
-
--spec add_list(binary(), [acl()], boolean()) -> ok | {error, any()}.
-
-add_list(Host, ACLs, Clear) ->
- {ResL, BadNodes} = ejabberd_cluster:multicall(
- ?MODULE, add_list_local,
- [Host, ACLs, Clear]),
- case lists:keyfind(aborted, 1, ResL) of
- false when BadNodes == [] ->
- ok;
- false ->
- {error, {failed_nodes, BadNodes}};
- Err ->
- {error, Err}
- end.
-
-add_list_local(Host, ACLs, Clear) ->
- F = fun () ->
- if Clear ->
- Ks = mnesia:select(acl,
- [{{acl, {'$1', Host}, '$2'}, [],
- ['$1']}]),
- lists:foreach(fun (K) -> mnesia:delete({acl, {K, Host}})
- end,
- Ks);
- true -> ok
- end,
- lists:foreach(fun (ACL) ->
- case ACL of
- #acl{aclname = ACLName,
- aclspec = ACLSpec} ->
- mnesia:write(#acl{aclname =
- {ACLName,
- Host},
- aclspec =
- normalize_spec(ACLSpec)})
- end
- end,
- ACLs)
- end,
- mnesia:transaction(F).
-
--spec add_access(binary() | global,
- access_name(), [access_rule()]) -> ok | {error, any()}.
-
-add_access(Host, Access, Rules) ->
- Obj = #access{name = {Access, Host}, rules = Rules},
- case mnesia:transaction(fun() -> mnesia:write(Obj) end) of
- {atomic, ok} ->
- ok;
- Err ->
- {error, Err}
- end.
-
--spec load_from_config() -> ok.
-
-load_from_config() ->
- Hosts = [global|?MYHOSTS],
- lists:foreach(
- fun(Host) ->
- ACLs = ejabberd_config:get_option(
- {acl, Host}, fun(V) -> V end, []),
- AccessRules = ejabberd_config:get_option(
- {access, Host}, fun(V) -> V end, []),
- AccessRulesNew = ejabberd_config:get_option(
- {access_rules, Host}, fun(V) -> V end, []),
- ShaperRules = ejabberd_config:get_option(
- {shaper_rules, Host}, fun(V) -> V end, []),
- lists:foreach(
- fun({ACLName, SpecList}) ->
- lists:foreach(
- fun({ACLType, ACLSpecs}) when is_list(ACLSpecs) ->
- lists:foreach(
- fun(ACLSpec) ->
- add(Host, ACLName,
- {ACLType, ACLSpec})
- end, lists:flatten(ACLSpecs));
- ({ACLType, ACLSpecs}) ->
- add(Host, ACLName, {ACLType, ACLSpecs})
- end, lists:flatten(SpecList))
- end, ACLs),
- lists:foreach(
- fun({Access, Rules}) ->
- NRules = lists:map(fun({ACL, Type}) ->
- {Type, [{acl, ACL}]}
- end, Rules),
- add_access(Host, Access, NRules ++ [{deny, [all]}])
- end, AccessRules),
- lists:foreach(
- fun({Access, Rules}) ->
- add_access(Host, Access, Rules)
- end, AccessRulesNew),
- lists:foreach(
- fun({Access, Rules}) ->
- add_access(Host, Access, Rules)
- end, ShaperRules)
- end, Hosts).
-
-%% Delete all previous set ACLs and Access rules
-clear() ->
- mnesia:clear_table(acl),
- mnesia:clear_table(access),
- ok.
-
-b(S) ->
- iolist_to_binary(S).
-
-nodeprep(S) ->
- jid:nodeprep(b(S)).
-
-nameprep(S) ->
- jid:nameprep(b(S)).
-
-resourceprep(S) ->
- jid:resourceprep(b(S)).
-
-split_user_server(Str, NormFunUsr, NormFunSrv) ->
- case binary:split(Str, <<"@">>) of
- [U, S] ->
- {NormFunUsr(U), NormFunSrv(S)};
- _ ->
- NormFunUsr(Str)
- end.
-
-normalize_spec(Spec) ->
- case Spec of
- all -> all;
- none -> none;
- {acl, N} -> {acl, N};
- {user, {U, S}} -> {user, {nodeprep(U), nameprep(S)}};
- {user, U} -> {user, split_user_server(U, fun nodeprep/1, fun nameprep/1)};
- {shared_group, {G, H}} -> {shared_group, {b(G), nameprep(H)}};
- {shared_group, G} -> {shared_group, split_user_server(G, fun b/1, fun nameprep/1)};
- {user_regexp, {UR, S}} -> {user_regexp, {b(UR), nameprep(S)}};
- {user_regexp, UR} -> {user_regexp, split_user_server(UR, fun b/1, fun nameprep/1)};
- {node_regexp, {UR, SR}} -> {node_regexp, {b(UR), b(SR)}};
- {user_glob, {UR, S}} -> {user_glob, {b(UR), nameprep(S)}};
- {user_glob, UR} -> {user_glob, split_user_server(UR, fun b/1, fun nameprep/1)};
- {node_glob, {UR, SR}} -> {node_glob, {b(UR), b(SR)}};
- {server, S} -> {server, nameprep(S)};
- {resource, R} -> {resource, resourceprep(R)};
- {server_regexp, SR} -> {server_regexp, b(SR)};
- {server_glob, S} -> {server_glob, b(S)};
- {resource_glob, R} -> {resource_glob, b(R)};
- {ip, {Net, Mask}} -> {ip, {Net, Mask}};
- {ip, S} ->
- case parse_ip_netmask(b(S)) of
- {ok, Net, Mask} ->
- {ip, {Net, Mask}};
- error ->
- ?INFO_MSG("Invalid network address: ~p", [S]),
- none
- end
- end.
-
--spec any_rules_allowed(global | binary(), access_name(),
- jid() | ljid() | inet:ip_address()) -> boolean().
-
-any_rules_allowed(Host, Access, Entity) ->
- lists:any(fun (Rule) ->
- allow == acl:match_rule(Host, Rule, Entity)
- end,
- Access).
-
--spec match_rule(global | binary(), access_name(),
- jid() | ljid() | inet:ip_address()) -> any().
-match_rule(Host, Access, IP) when tuple_size(IP) == 4;
- tuple_size(IP) == 8 ->
- access_matches(Access, #{ip => IP}, Host);
+-type state() :: #{hosts := [binary()]}.
+-type action() :: allow | deny.
+-type ip_mask() :: {inet:ip4_address(), 0..32} | {inet:ip6_address(), 0..128}.
+-type access_rule() :: {acl, atom()} | acl_rule().
+-type acl_rule() :: {user, {binary(), binary()} | binary()} |
+ {server, binary()} |
+ {resource, binary()} |
+ {user_regexp, {re:mp(), binary()} | re:mp()} |
+ {server_regexp, re:mp()} |
+ {resource_regexp, re:mp()} |
+ {node_regexp, {re:mp(), re:mp()}} |
+ {user_glob, {re:mp(), binary()} | re:mp()} |
+ {server_glob, re:mp()} |
+ {resource_glob, re:mp()} |
+ {node_glob, {re:mp(), re:mp()}} |
+ {shared_group, {binary(), binary()} | binary()} |
+ {ip, ip_mask()}.
+-type access() :: [{action(), [access_rule()]}].
+-type acl() :: atom() | access().
+-type match() :: #{ip => inet:ip_address(),
+ usr => jid:ljid(),
+ atom() => term()}.
+
+-export_type([acl/0, acl_rule/0, access/0, access_rule/0, match/0]).
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+-spec match_rule(global | binary(), atom() | access(),
+ jid:jid() | jid:ljid() | inet:ip_address() | match()) -> action().
+match_rule(_, all, _) ->
+ allow;
+match_rule(_, none, _) ->
+ deny;
+match_rule(Host, Access, Match) when is_map(Match) ->
+ Rules = if is_atom(Access) -> read_access(Access, Host);
+ true -> Access
+ end,
+ match_rules(Host, Rules, Match, deny);
+match_rule(Host, Access, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 ->
+ match_rule(Host, Access, #{ip => IP});
match_rule(Host, Access, JID) ->
- access_matches(Access, #{usr => jid:tolower(JID)}, Host).
-
--spec acl_rule_verify(aclspec()) -> boolean().
+ match_rule(Host, Access, #{usr => jid:tolower(JID)}).
-acl_rule_verify(all) ->
- true;
-acl_rule_verify(none) ->
- true;
-acl_rule_verify({ip, {{A,B,C,D}, Mask}})
- when is_integer(A), is_integer(B), is_integer(C), is_integer(D),
- A >= 0, A =< 255, B >= 0, B =< 255, C >= 0, C =< 255, D >= 0, D =< 255,
- is_integer(Mask), Mask >= 0, Mask =< 32 ->
- true;
-acl_rule_verify({ip, {{A,B,C,D,E,F,G,H}, Mask}}) when
- is_integer(A), is_integer(B), is_integer(C), is_integer(D),
- is_integer(E), is_integer(F), is_integer(G), is_integer(H),
- A >= 0, A =< 65535, B >= 0, B =< 65535, C >= 0, C =< 65535, D >= 0, D =< 65535,
- E >= 0, E =< 65535, F >= 0, F =< 65535, G >= 0, G =< 65535, H >= 0, H =< 65535,
- is_integer(Mask), Mask >= 0, Mask =< 64 ->
- true;
-acl_rule_verify({user, {U, S}}) when is_binary(U), is_binary(S) ->
- true;
-acl_rule_verify({user, U}) when is_binary(U) ->
- true;
-acl_rule_verify({server, S}) when is_binary(S) ->
- true;
-acl_rule_verify({resource, R}) when is_binary(R) ->
- true;
-acl_rule_verify({shared_group, {G, H}}) when is_binary(G), is_binary(H) ->
- true;
-acl_rule_verify({shared_group, G}) when is_binary(G) ->
- true;
-acl_rule_verify({user_regexp, {UR, S}}) when is_binary(UR), is_binary(S) ->
- true;
-acl_rule_verify({user_regexp, UR}) when is_binary(UR) ->
- true;
-acl_rule_verify({server_regexp, SR}) when is_binary(SR) ->
- true;
-acl_rule_verify({resource_regexp, RR}) when is_binary(RR) ->
- true;
-acl_rule_verify({node_regexp, {UR, SR}}) when is_binary(UR), is_binary(SR) ->
- true;
-acl_rule_verify({user_glob, {UR, S}}) when is_binary(UR), is_binary(S) ->
- true;
-acl_rule_verify({user_glob, UR}) when is_binary(UR) ->
- true;
-acl_rule_verify({server_glob, SR}) when is_binary(SR) ->
- true;
-acl_rule_verify({resource_glob, RR}) when is_binary(RR) ->
+-spec match_acl(global | binary(), access_rule(), match()) -> boolean().
+match_acl(_Host, {acl, all}, _) ->
true;
-acl_rule_verify({node_glob, {UR, SR}}) when is_binary(UR), is_binary(SR) ->
- true;
-acl_rule_verify(_Spec) ->
- false.
-invalid_syntax(Msg, Data) ->
- throw({invalid_syntax, iolist_to_binary(io_lib:format(Msg, Data))}).
-
-acl_rules_verify([{acl, Name} | Rest], true) when is_atom(Name) ->
- acl_rules_verify(Rest, true);
-acl_rules_verify([{acl, Name} = Rule | _Rest], false) when is_atom(Name) ->
- invalid_syntax(<<"Using acl: rules not allowed: ~p">>, [Rule]);
-acl_rules_verify([Rule | Rest], AllowAcl) ->
- case acl_rule_verify(Rule) of
- false ->
- invalid_syntax(<<"Invalid rule: ~p">>, [Rule]);
- true ->
- acl_rules_verify(Rest, AllowAcl)
- end;
-acl_rules_verify([], _AllowAcl) ->
- true;
-acl_rules_verify(Rules, _AllowAcl) ->
- invalid_syntax(<<"Not a acl rules list: ~p">>, [Rules]).
-
-
-
-all_acl_rules_matches([], _Data, _Host) ->
- false;
-all_acl_rules_matches(Rules, Data, Host) ->
- all_acl_rules_matches2(Rules, Data, Host).
-
-all_acl_rules_matches2([Rule | Tail], Data, Host) ->
- case acl_rule_matches(Rule, Data, Host) of
- true ->
- all_acl_rules_matches2(Tail, Data, Host);
- false ->
- false
- end;
-all_acl_rules_matches2([], _Data, _Host) ->
- true.
-
-any_acl_rules_matches([], _Data, _Host) ->
+match_acl(_Host, {acl, none}, _) ->
false;
-any_acl_rules_matches([Rule|Tail], Data, Host) ->
- case acl_rule_matches(Rule, Data, Host) of
- true ->
- true;
- false ->
- any_acl_rules_matches(Tail, Data, Host)
- end.
-
--spec acl_rule_matches(aclspec(), any(), global|binary()) -> boolean().
-
-acl_rule_matches(all, _Data, _Host) ->
- true;
-acl_rule_matches({acl, all}, _Data, _Host) ->
- true;
-acl_rule_matches({acl, Name}, Data, Host) ->
- ACLs = get_aclspecs(Name, Host),
- RawACLs = lists:map(fun(#acl{aclspec = R}) -> R end, ACLs),
- any_acl_rules_matches(RawACLs, Data, Host);
-acl_rule_matches({ip, {Net, Mask}}, #{ip := {IP, _Port}}, _Host) ->
- is_ip_match(IP, Net, Mask);
-acl_rule_matches({ip, {Net, Mask}}, #{ip := IP}, _Host) ->
- is_ip_match(IP, Net, Mask);
-acl_rule_matches({user, {U, S}}, #{usr := {U, S, _}}, _Host) ->
+match_acl(Host, {acl, ACLName}, Match) ->
+ lists:any(
+ fun(ACL) ->
+ match_acl(Host, ACL, Match)
+ end, read_acl(ACLName, Host));
+match_acl(_Host, {ip, {Net, Mask}}, #{ip := {IP, _Port}}) ->
+ misc:match_ip_mask(IP, Net, Mask);
+match_acl(_Host, {ip, {Net, Mask}}, #{ip := IP}) ->
+ misc:match_ip_mask(IP, Net, Mask);
+match_acl(_Host, {user, {U, S}}, #{usr := {U, S, _}}) ->
true;
-acl_rule_matches({user, U}, #{usr := {U, S, _}}, _Host) ->
- lists:member(S, ?MYHOSTS);
-acl_rule_matches({server, S}, #{usr := {_, S, _}}, _Host) ->
+match_acl(_Host, {user, U}, #{usr := {U, S, _}}) ->
+ ejabberd_router:is_my_host(S);
+match_acl(_Host, {server, S}, #{usr := {_, S, _}}) ->
true;
-acl_rule_matches({resource, R}, #{usr := {_, _, R}}, _Host) ->
+match_acl(_Host, {resource, R}, #{usr := {_, _, R}}) ->
true;
-acl_rule_matches({shared_group, {G, H}}, #{usr := {U, S, _}}, _Host) ->
- Mod = loaded_shared_roster_module(H),
- Mod:is_user_in_group({U, S}, G, H);
-acl_rule_matches({shared_group, G}, #{usr := {U, S, _}}, Host) ->
- Mod = loaded_shared_roster_module(Host),
- Mod:is_user_in_group({U, S}, G, Host);
-acl_rule_matches({user_regexp, {UR, S}}, #{usr := {U, S, _}}, _Host) ->
- is_regexp_match(U, UR);
-acl_rule_matches({user_regexp, UR}, #{usr := {U, S, _}}, _Host) ->
- lists:member(S, ?MYHOSTS) andalso is_regexp_match(U, UR);
-acl_rule_matches({server_regexp, SR}, #{usr := {_, S, _}}, _Host) ->
- is_regexp_match(S, SR);
-acl_rule_matches({resource_regexp, RR}, #{usr := {_, _, R}}, _Host) ->
- is_regexp_match(R, RR);
-acl_rule_matches({node_regexp, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
- is_regexp_match(U, UR) andalso is_regexp_match(S, SR);
-acl_rule_matches({user_glob, {UR, S}}, #{usr := {U, S, _}}, _Host) ->
- is_glob_match(U, UR);
-acl_rule_matches({user_glob, UR}, #{usr := {U, S, _}}, _Host) ->
- lists:member(S, ?MYHOSTS) andalso is_glob_match(U, UR);
-acl_rule_matches({server_glob, SR}, #{usr := {_, S, _}}, _Host) ->
- is_glob_match(S, SR);
-acl_rule_matches({resource_glob, RR}, #{usr := {_, _, R}}, _Host) ->
- is_glob_match(R, RR);
-acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
- is_glob_match(U, UR) andalso is_glob_match(S, SR);
-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) ->
- GAccess = ets:lookup(access, {Name, global}),
- LAccess =
- if Host /= global -> ets:lookup(access, {Name, Host});
- true -> []
- end,
- case GAccess ++ LAccess of
- [] ->
- deny;
- AccessList ->
- Rules = lists:flatmap(
- fun(#access{rules = Rs}) ->
- Rs
- end, AccessList),
- access_rules_matches(Rules, Data, Host)
+match_acl(_Host, {shared_group, {G, H}}, #{usr := {U, S, _}}) ->
+ case loaded_shared_roster_module(H) of
+ undefined -> false;
+ Mod -> Mod:is_user_in_group({U, S}, G, H)
end;
-access_matches(Rules, Data, Host) when is_list(Rules) ->
- access_rules_matches(Rules, Data, Host).
-
-
--spec access_rules_matches(list(), any(), global|binary()) -> any().
-
-access_rules_matches(AR, Data, Host) ->
- access_rules_matches(AR, Data, Host, deny).
+match_acl(Host, {shared_group, G}, Map) ->
+ match_acl(Host, {shared_group, {G, Host}}, Map);
+match_acl(_Host, {user_regexp, {UR, S}}, #{usr := {U, S, _}}) ->
+ match_regexp(U, UR);
+match_acl(_Host, {user_regexp, UR}, #{usr := {U, S, _}}) ->
+ ejabberd_router:is_my_host(S) andalso match_regexp(U, UR);
+match_acl(_Host, {server_regexp, SR}, #{usr := {_, S, _}}) ->
+ match_regexp(S, SR);
+match_acl(_Host, {resource_regexp, RR}, #{usr := {_, _, R}}) ->
+ match_regexp(R, RR);
+match_acl(_Host, {node_regexp, {UR, SR}}, #{usr := {U, S, _}}) ->
+ match_regexp(U, UR) andalso match_regexp(S, SR);
+match_acl(_Host, {user_glob, {UR, S}}, #{usr := {U, S, _}}) ->
+ match_regexp(U, UR);
+match_acl(_Host, {user_glob, UR}, #{usr := {U, S, _}}) ->
+ ejabberd_router:is_my_host(S) andalso match_regexp(U, UR);
+match_acl(_Host, {server_glob, SR}, #{usr := {_, S, _}}) ->
+ match_regexp(S, SR);
+match_acl(_Host, {resource_glob, RR}, #{usr := {_, _, R}}) ->
+ match_regexp(R, RR);
+match_acl(_Host, {node_glob, {UR, SR}}, #{usr := {U, S, _}}) ->
+ match_regexp(U, UR) andalso match_regexp(S, SR);
+match_acl(_, _, _) ->
+ false.
-access_rules_matches([{Type, Acls} | Rest], Data, Host, Default) ->
- case all_acl_rules_matches(Acls, Data, Host) of
+-spec match_rules(global | binary(), [{T, [access_rule()]}], match(), T) -> T.
+match_rules(Host, [{Return, Rules} | Rest], Match, Default) ->
+ case match_acls(Host, Rules, Match) of
false ->
- access_rules_matches(Rest, Data, Host, Default);
+ match_rules(Host, Rest, Match, Default);
true ->
- Type
+ Return
end;
-access_rules_matches([], _Data, _Host, Default) ->
+match_rules(_Host, [], _Match, Default) ->
Default.
-get_aclspecs(ACL, Host) ->
- ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}).
-
-is_regexp_match(String, RegExp) ->
- case ejabberd_regexp:run(String, RegExp) of
- nomatch -> false;
- match -> true;
- {error, ErrDesc} ->
- ?ERROR_MSG("Wrong regexp ~p in ACL: ~p",
- [RegExp, ErrDesc]),
- false
+-spec match_acls(global | binary(), [access_rule()], match()) -> boolean().
+match_acls(_Host, [], _Match) ->
+ false;
+match_acls(Host, Rules, Match) ->
+ lists:all(
+ fun(Rule) ->
+ match_acl(Host, Rule, Match)
+ end, Rules).
+
+-spec reload_from_config() -> ok.
+reload_from_config() ->
+ gen_server:call(?MODULE, reload_from_config, timer:minutes(1)).
+
+-spec validator(access_rules | acl) -> econf:validator().
+validator(access_rules) ->
+ econf:options(
+ #{'_' => access_rules_validator()},
+ [{disallowed, [all, none]}, unique]);
+validator(acl) ->
+ econf:options(
+ #{'_' => acl_validator()},
+ [{disallowed, [all, none]}, unique]).
+
+%%%===================================================================
+%%% gen_server callbacks
+%%%===================================================================
+-spec init([]) -> {ok, state()}.
+init([]) ->
+ create_tab(acl),
+ create_tab(access),
+ Hosts = ejabberd_option:hosts(),
+ load_from_config(Hosts),
+ ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20),
+ {ok, #{hosts => Hosts}}.
+
+-spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}.
+handle_call(reload_from_config, _, State) ->
+ NewHosts = ejabberd_option:hosts(),
+ load_from_config(NewHosts),
+ {reply, ok, State#{hosts => NewHosts}};
+handle_call(Request, From, State) ->
+ ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
+ {noreply, State}.
+
+-spec handle_cast(term(), state()) -> {noreply, state()}.
+handle_cast(Msg, State) ->
+ ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
+ {noreply, State}.
+
+-spec handle_info(term(), state()) -> {noreply, state()}.
+handle_info(Info, State) ->
+ ?WARNING_MSG("Unexpected info: ~p", [Info]),
+ {noreply, State}.
+
+-spec terminate(any(), state()) -> ok.
+terminate(_Reason, _State) ->
+ ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20).
+
+-spec code_change(term(), state(), term()) -> {ok, state()}.
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+%%%===================================================================
+%%% Table management
+%%%===================================================================
+-spec load_from_config([binary()]) -> ok.
+load_from_config(NewHosts) ->
+ ?DEBUG("Loading access rules from config", []),
+ load_tab(acl, NewHosts, fun ejabberd_option:acl/1),
+ load_tab(access, NewHosts, fun ejabberd_option:access_rules/1),
+ ?DEBUG("Access rules loaded successfully", []).
+
+-spec create_tab(atom()) -> atom().
+create_tab(Tab) ->
+ _ = mnesia:delete_table(Tab),
+ ets:new(Tab, [named_table, set, {read_concurrency, true}]).
+
+-spec load_tab(atom(), [binary()], fun((global | binary()) -> {atom(), list()})) -> ok.
+load_tab(Tab, Hosts, Fun) ->
+ Old = ets:tab2list(Tab),
+ New = lists:flatmap(
+ fun(Host) ->
+ [{{Name, Host}, List} || {Name, List} <- Fun(Host)]
+ end, [global|Hosts]),
+ ets:insert(Tab, New),
+ lists:foreach(
+ fun({Key, _}) ->
+ case lists:keymember(Key, 1, New) of
+ false -> ets:delete(Tab, Key);
+ true -> ok
+ end
+ end, Old).
+
+-spec read_access(atom(), global | binary()) -> access().
+read_access(Name, Host) ->
+ case ets:lookup(access, {Name, Host}) of
+ [{_, Access}] -> Access;
+ [] -> []
end.
-is_glob_match(String, Glob) ->
- is_regexp_match(String,
- ejabberd_regexp:sh_to_awk(Glob)).
-
-is_ip_match({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) ->
- IPInt = ip_to_integer(IP),
- NetInt = ip_to_integer(Net),
- M = bnot (1 bsl (32 - Mask) - 1),
- IPInt band M =:= NetInt band M;
-is_ip_match({_, _, _, _, _, _, _, _} = IP,
- {_, _, _, _, _, _, _, _} = Net, Mask) ->
- IPInt = ip_to_integer(IP),
- NetInt = ip_to_integer(Net),
- M = bnot (1 bsl (128 - Mask) - 1),
- IPInt band M =:= NetInt band M;
-is_ip_match(_, _, _) ->
- false.
-
-ip_to_integer({IP1, IP2, IP3, IP4}) ->
- IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4;
-ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7,
- IP8}) ->
- IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16
- bor IP5
- bsl 16
- bor IP6
- bsl 16
- bor IP7
- bsl 16
- bor IP8.
+-spec read_acl(atom(), global | binary()) -> [acl_rule()].
+read_acl(Name, Host) ->
+ case ets:lookup(acl, {Name, Host}) of
+ [{_, ACL}] -> ACL;
+ [] -> []
+ end.
+%%%===================================================================
+%%% Validators
+%%%===================================================================
+validators() ->
+ #{ip => econf:list_or_single(econf:ip_mask()),
+ user => user_validator(econf:user(), econf:domain()),
+ user_regexp => user_validator(econf:re([unicode]), econf:domain()),
+ user_glob => user_validator(econf:glob([unicode]), econf:domain()),
+ server => econf:list_or_single(econf:domain()),
+ server_regexp => econf:list_or_single(econf:re([unicode])),
+ server_glob => econf:list_or_single(econf:glob([unicode])),
+ resource => econf:list_or_single(econf:resource()),
+ resource_regexp => econf:list_or_single(econf:re([unicode])),
+ resource_glob => econf:list_or_single(econf:glob([unicode])),
+ node_regexp => node_validator(econf:re([unicode]), econf:re([unicode])),
+ node_glob => node_validator(econf:glob([unicode]), econf:glob([unicode])),
+ shared_group => user_validator(econf:binary(), econf:domain()),
+ acl => econf:atom()}.
+
+rule_validator() ->
+ rule_validator(validators()).
+
+rule_validator(RVs) ->
+ econf:and_then(
+ econf:non_empty(econf:options(RVs, [])),
+ fun(Rules) ->
+ lists:flatmap(
+ fun({Type, Rs}) when is_list(Rs) ->
+ [{Type, R} || R <- Rs];
+ (Other) ->
+ [Other]
+ end, Rules)
+ end).
+
+access_validator() ->
+ 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,
+ rule_validator()).
+
+access_rules_validator() ->
+ econf:and_then(
+ fun(L) when is_list(L) ->
+ lists:map(
+ fun({K, V}) -> {(econf:atom())(K), V};
+ (A) -> {(econf:atom())(A), [{acl, all}]}
+ end, lists:flatten(L));
+ (Bad) ->
+ Bad
+ end,
+ econf:non_empty(
+ econf:options(
+ #{allow => access_validator(),
+ deny => access_validator()},
+ []))).
+
+acl_validator() ->
+ econf:and_then(
+ fun(L) when is_list(L) -> lists:flatten(L);
+ (Bad) -> Bad
+ end,
+ rule_validator(maps:remove(acl, validators()))).
+
+user_validator(UV, SV) ->
+ econf:and_then(
+ econf:list_or_single(
+ fun({U, S}) ->
+ {UV(U), SV(S)};
+ (M) when is_list(M) ->
+ (econf:map(UV, SV))(M);
+ (Val) ->
+ US = (econf:binary())(Val),
+ case binary:split(US, <<"@">>, [global]) of
+ [U, S] -> {UV(U), SV(S)};
+ [U] -> UV(U);
+ _ -> econf:fail({bad_user, Val})
+ end
+ end),
+ fun lists:flatten/1).
+
+node_validator(UV, SV) ->
+ econf:and_then(
+ econf:and_then(
+ econf:list(econf:any()),
+ fun lists:flatten/1),
+ econf:map(UV, SV)).
+
+%%%===================================================================
+%%% Aux
+%%%===================================================================
+-spec match_regexp(iodata(), re:mp()) -> boolean().
+match_regexp(Data, RegExp) ->
+ re:run(Data, RegExp) /= nomatch.
+
+-spec loaded_shared_roster_module(global | binary()) -> atom().
+loaded_shared_roster_module(global) ->
+ loaded_shared_roster_module(ejabberd_config:get_myname());
loaded_shared_roster_module(Host) ->
case gen_mod:is_loaded(Host, mod_shared_roster_ldap) of
- true -> mod_shared_roster_ldap;
- false -> mod_shared_roster
- end.
-
-parse_ip_netmask(S) ->
- case str:tokens(S, <<"/">>) of
- [IPStr] ->
- case inet_parse:address(binary_to_list(IPStr)) of
- {ok, {_, _, _, _} = IP} -> {ok, IP, 32};
- {ok, {_, _, _, _, _, _, _, _} = IP} -> {ok, IP, 128};
- _ -> error
- end;
- [IPStr, MaskStr] ->
- case catch jlib:binary_to_integer(MaskStr) of
- Mask when is_integer(Mask), Mask >= 0 ->
- case inet_parse:address(binary_to_list(IPStr)) of
- {ok, {_, _, _, _} = IP} when Mask =< 32 ->
- {ok, IP, Mask};
- {ok, {_, _, _, _, _, _, _, _} = IP} when Mask =< 128 ->
- {ok, IP, Mask};
- _ -> error
- end;
- _ -> error
- end;
- _ -> error
- end.
-
-transform_access_rules_config(Config) when is_list(Config) ->
- lists:map(fun transform_access_rules_config2/1, lists:flatten(Config));
-transform_access_rules_config(Config) ->
- transform_access_rules_config([Config]).
-
-transform_access_rules_config2(Type) when is_integer(Type); is_atom(Type) ->
- {Type, [all]};
-transform_access_rules_config2({Type, ACL}) when is_atom(ACL) ->
- {Type, [{acl, ACL}]};
-transform_access_rules_config2({Res, Rules}) when is_list(Rules) ->
- T = lists:map(fun({Type, Args}) when is_list(Args) ->
- normalize_spec({Type, hd(lists:flatten(Args))});
- (V) -> normalize_spec(V)
- end, lists:flatten(Rules)),
- {Res, T};
-transform_access_rules_config2({Res, Rule}) ->
- {Res, [Rule]}.
-
-access_rules_validator(Name) when is_atom(Name) ->
- Name;
-access_rules_validator(Rules0) ->
- Rules = transform_access_rules_config(Rules0),
- access_shaper_rules_validator(Rules, fun(allow) -> true;
- (deny) -> true;
- (_) -> false
- end),
- throw({replace_with, Rules}).
-
-
-shaper_rules_validator(Name) when is_atom(Name) ->
- Name;
-shaper_rules_validator(Rules0) ->
- Rules = transform_access_rules_config(Rules0),
- access_shaper_rules_validator(Rules, fun(V) when is_atom(V) -> true;
- (V2) when is_integer(V2) -> true;
- (_) -> false
- end),
- throw({replace_with, Rules}).
-
-access_shaper_rules_validator([{Type, Acls} = Rule | Rest], RuleTypeCheck) ->
- case RuleTypeCheck(Type) of
- true ->
- case acl_rules_verify(Acls, true) of
- true ->
- access_shaper_rules_validator(Rest, RuleTypeCheck);
- Err ->
- Err
- end;
+ true -> mod_shared_roster_ldap;
false ->
- invalid_syntax(<<"Invalid rule type: ~p in rule ~p">>, [Type, Rule])
- end;
-access_shaper_rules_validator([], _RuleTypeCheck) ->
- true;
-access_shaper_rules_validator(Value, _RuleTypeCheck) ->
- invalid_syntax(<<"Not a rule definition: ~p">>, [Value]).
-
-
-transform_options(Opts) ->
- Opts1 = lists:foldl(fun transform_options/2, [], Opts),
- {ACLOpts, Opts2} = lists:mapfoldl(
- fun({acl, Os}, Acc) ->
- {Os, Acc};
- (O, Acc) ->
- {[], [O|Acc]}
- end, [], Opts1),
- {AccessOpts, Opts3} = lists:mapfoldl(
- fun({access, Os}, Acc) ->
- {Os, Acc};
- (O, Acc) ->
- {[], [O|Acc]}
- end, [], Opts2),
- {NewAccessOpts, Opts4} = lists:mapfoldl(
- fun({access_rules, Os}, Acc) ->
- {Os, Acc};
- (O, Acc) ->
- {[], [O|Acc]}
- end, [], Opts3),
- {ShaperOpts, Opts5} = lists:mapfoldl(
- fun({shaper_rules, Os}, Acc) ->
- {Os, Acc};
- (O, Acc) ->
- {[], [O|Acc]}
- end, [], Opts4),
- ACLOpts1 = ejabberd_config:collect_options(lists:flatten(ACLOpts)),
- AccessOpts1 = case ejabberd_config:collect_options(
- lists:flatten(AccessOpts)) of
- [] -> [];
- L1 -> [{access, L1}]
- end,
- ACLOpts2 = case lists:map(
- fun({ACLName, Os}) ->
- {ACLName, ejabberd_config:collect_options(Os)}
- end, ACLOpts1) of
- [] -> [];
- L2 -> [{acl, L2}]
- end,
- NewAccessOpts1 = case lists:map(
- fun({NAName, Os}) ->
- {NAName, transform_access_rules_config(Os)}
- end, lists:flatten(NewAccessOpts)) of
- [] -> [];
- L3 -> [{access_rules, L3}]
- end,
- ShaperOpts1 = case lists:map(
- fun({SName, Ss}) ->
- {SName, transform_access_rules_config(Ss)}
- end, lists:flatten(ShaperOpts)) of
- [] -> [];
- L4 -> [{shaper_rules, L4}]
- end,
- ACLOpts2 ++ AccessOpts1 ++ NewAccessOpts1 ++ ShaperOpts1 ++ Opts5.
-
-transform_options({acl, Name, Type}, Opts) ->
- T = case Type of
- all -> all;
- none -> none;
- {user, U} -> {user, [b(U)]};
- {user, U, S} -> {user, [[{b(U), b(S)}]]};
- {shared_group, G} -> {shared_group, [b(G)]};
- {shared_group, G, H} -> {shared_group, [[{b(G), b(H)}]]};
- {user_regexp, UR} -> {user_regexp, [b(UR)]};
- {user_regexp, UR, S} -> {user_regexp, [[{b(UR), b(S)}]]};
- {node_regexp, UR, SR} -> {node_regexp, [[{b(UR), b(SR)}]]};
- {user_glob, UR} -> {user_glob, [b(UR)]};
- {user_glob, UR, S} -> {user_glob, [[{b(UR), b(S)}]]};
- {node_glob, UR, SR} -> {node_glob, [[{b(UR), b(SR)}]]};
- {server, S} -> {server, [b(S)]};
- {resource, R} -> {resource, [b(R)]};
- {server_regexp, SR} -> {server_regexp, [b(SR)]};
- {server_glob, S} -> {server_glob, [b(S)]};
- {ip, S} -> {ip, [b(S)]};
- {resource_glob, R} -> {resource_glob, [b(R)]}
- end,
- [{acl, [{Name, [T]}]}|Opts];
-transform_options({access, Name, Rules}, Opts) ->
- NewRules = [{ACL, Action} || {Action, ACL} <- Rules],
- [{access, [{Name, NewRules}]}|Opts];
-transform_options(Opt, Opts) ->
- [Opt|Opts].
-
-opt_type(access) -> fun (V) -> V end;
-opt_type(access_rules) -> fun (V) -> V end;
-opt_type(shaper_rules) -> fun (V) -> V end;
-opt_type(acl) -> fun (V) -> V end;
-opt_type(_) -> [access, acl, access_rules, shaper_rules].
+ case gen_mod:is_loaded(Host, mod_shared_roster) of
+ true -> mod_shared_roster;
+ false -> undefined
+ end
+ end.