diff options
Diffstat (limited to 'src/acl.erl')
-rw-r--r-- | src/acl.erl | 985 |
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. |