%%%---------------------------------------------------------------------- %%% File : acl.erl %%% Author : Alexey Shchepin %%% Purpose : ACL support %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2013 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., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%---------------------------------------------------------------------- -module(acl). -author('alexey@process-one.net'). -export([start/0, to_record/3, add/3, add_list/3, add_local/3, add_list_local/3, load_from_config/0, match_rule/3, match_acl/3, transform_options/1]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). -record(acl, {aclname, aclspec}). -record(access, {name :: access_name(), rules = [] :: [access_rule()]}). -type regexp() :: 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, {inet:ip_address(), integer()}} | {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 to_record(binary(), atom(), aclspec()) -> acl(). to_record(Host, ACLName, ACLSpec) -> #acl{aclname = {ACLName, Host}, aclspec = normalize_spec(ACLSpec)}. -spec add(binary(), aclname(), aclspec()) -> ok | {error, any()}. add(Host, ACLName, ACLSpec) -> {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), ?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} = rpc:multicall(mnesia:system_info(running_db_nodes), ?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) -> case mnesia:transaction( fun() -> mnesia:write( #access{name = {Access, Host}, rules = Rules}) 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, []), 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}) -> add_access(Host, Access, Rules) end, AccessRules) end, Hosts). b(S) -> iolist_to_binary(S). nodeprep(S) -> jlib:nodeprep(b(S)). nameprep(S) -> jlib:nameprep(b(S)). resourceprep(S) -> jlib:resourceprep(b(S)). normalize_spec(Spec) -> case Spec of all -> all; none -> none; {user, {U, S}} -> {user, {nodeprep(U), nameprep(S)}}; {user, U} -> {user, nodeprep(U)}; {shared_group, {G, H}} -> {shared_group, {b(G), nameprep(H)}}; {shared_group, G} -> {shared_group, b(G)}; {user_regexp, {UR, S}} -> {user_regexp, {b(UR), nameprep(S)}}; {user_regexp, UR} -> {user_regexp, b(UR)}; {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, b(UR)}; {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, 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 match_rule(global | binary(), access_name(), jid() | ljid() | inet:ip_address()) -> any(). match_rule(_Host, all, _JID) -> allow; match_rule(_Host, none, _JID) -> deny; match_rule(Host, Access, JID) -> GAccess = ets:lookup(access, {Access, global}), LAccess = if Host /= global -> ets:lookup(access, {Access, Host}); true -> [] end, case GAccess ++ LAccess of [] -> ?WARNING_MSG("Attempt to match against unspecified " "access rule '~s' (scope: ~s)", [Access, Host]), deny; AccessList -> Rules = lists:flatmap( fun(#access{rules = Rs}) -> Rs end, AccessList), match_acls(Rules, JID, Host) end. match_acls([], _, _Host) -> deny; match_acls([{ACL, Access} | ACLs], JID, Host) -> case match_acl(ACL, JID, Host) of true -> Access; _ -> match_acls(ACLs, JID, Host) end. -spec match_acl(atom(), jid() | ljid() | inet:ip_address(), binary()) -> boolean(). match_acl(all, _JID, _Host) -> true; match_acl(none, _JID, _Host) -> false; match_acl(ACL, IP, Host) when tuple_size(IP) == 4; tuple_size(IP) == 8 -> lists:any( fun(#acl{aclspec = {ip, {Net, Mask}}}) -> is_ip_match(IP, Net, Mask); (_) -> false end, ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global})); match_acl(ACL, JID, Host) -> {User, Server, Resource} = jlib:jid_tolower(JID), lists:any( fun(#acl{aclspec = Spec}) -> case Spec of all -> true; {user, {U, S}} -> U == User andalso S == Server; {user, U} -> U == User andalso lists:member(Server, ?MYHOSTS); {server, S} -> S == Server; {resource, R} -> R == Resource; {shared_group, {G, H}} -> Mod = loaded_shared_roster_module(H), Mod:is_user_in_group({User, Server}, G, H); {shared_group, G} -> Mod = loaded_shared_roster_module(Host), Mod:is_user_in_group({User, Server}, G, Host); {user_regexp, {UR, S}} -> S == Server andalso is_regexp_match(User, UR); {user_regexp, UR} -> lists:member(Server, ?MYHOSTS) andalso is_regexp_match(User, UR); {server_regexp, SR} -> is_regexp_match(Server, SR); {resource_regexp, RR} -> is_regexp_match(Resource, RR); {node_regexp, {UR, SR}} -> is_regexp_match(Server, SR) andalso is_regexp_match(User, UR); {user_glob, {UR, S}} -> S == Server andalso is_glob_match(User, UR); {user_glob, UR} -> lists:member(Server, ?MYHOSTS) andalso is_glob_match(User, UR); {server_glob, SR} -> is_glob_match(Server, SR); {resource_glob, RR} -> is_glob_match(Resource, RR); {node_glob, {UR, SR}} -> is_glob_match(Server, SR) andalso is_glob_match(User, UR); WrongSpec -> ?ERROR_MSG("Wrong ACL expression: ~p~nCheck your " "config file and reload it with the override_a" "cls option enabled", [WrongSpec]), false end end, 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 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. 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_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), 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, ACLOpts2 ++ AccessOpts1 ++ Opts3. transform_options({acl, Name, Type}, Opts) -> T = case Type of all -> all; none -> none; {user, U} -> {user, [U]}; {user, U, S} -> {user, [[{U, S}]]}; {shared_group, G} -> {shared_group, [G]}; {shared_group, G, H} -> {shared_group, [[{G, H}]]}; {user_regexp, UR} -> {user_regexp, [UR]}; {user_regexp, UR, S} -> {user_regexp, [[{UR, S}]]}; {node_regexp, UR, SR} -> {node_regexp, [[{UR, SR}]]}; {user_glob, UR} -> {user_glob, [UR]}; {user_glob, UR, S} -> {user_glob, [[{UR, S}]]}; {node_glob, UR, SR} -> {node_glob, [[{UR, SR}]]}; {server, S} -> {server, [S]}; {resource, R} -> {resource, [R]}; {server_regexp, SR} -> {server_regexp, [SR]}; {server_glob, S} -> {server_glob, [S]}; {ip, S} -> {ip, [S]}; {resource_glob, R} -> {resource_glob, [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].