diff options
Diffstat (limited to 'src/acl.erl')
-rw-r--r-- | src/acl.erl | 469 |
1 files changed, 315 insertions, 154 deletions
diff --git a/src/acl.erl b/src/acl.erl index 1338e55b6..4c4523617 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -29,35 +29,38 @@ -author('alexey@process-one.net'). -export([start/0, to_record/3, add/3, add_list/3, - add_local/3, add_list_local/3, - match_rule/3, match_acl/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()} | - {user, binary(), binary()} | + {user, {binary(), host()} | binary()} | {server, binary()} | {resource, binary()} | - {user_regexp, regexp()} | - {shared_group, binary()} | - {shared_group, binary(), binary()} | - {user_regexp, regexp(), 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()} | - {user_glob, glob(), binary()} | + {node_regexp, {regexp(), regexp()}} | + {user_glob, {glob(), host()} | glob()} | {server_glob, glob()} | {resource_glob, glob()} | - {node_glob, glob(), glob()}. + {ip, {inet:ip_address(), integer()}} | + {node_glob, {glob(), glob()}}. -type acl() :: #acl{aclname :: aclname(), aclspec :: aclspec()}. @@ -65,12 +68,23 @@ -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, - [{disc_copies, [node()]}, {type, bag}, + [{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), - update_table(), + mnesia:add_table_copy(access, node(), ram_copies), + load_from_config(), ok. -spec to_record(binary(), atom(), aclspec()) -> acl(). @@ -82,7 +96,7 @@ to_record(Host, ACLName, ACLSpec) -> -spec add(binary(), aclname(), aclspec()) -> ok | {error, any()}. add(Host, ACLName, ACLSpec) -> - {ResL, BadNodes} = rpc:multicall(ejabberd_cluster:get_nodes(), + {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), ?MODULE, add_local, [Host, ACLName, ACLSpec]), case lists:keyfind(aborted, 1, ResL) of @@ -109,7 +123,7 @@ add_local(Host, ACLName, ACLSpec) -> -spec add_list(binary(), [acl()], boolean()) -> ok | {error, any()}. add_list(Host, ACLs, Clear) -> - {ResL, BadNodes} = rpc:multicall(ejabberd_cluster:get_nodes(), + {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), ?MODULE, add_list_local, [Host, ACLs, Clear]), case lists:keyfind(aborted, 1, ResL) of @@ -147,130 +161,196 @@ add_list_local(Host, ACLs, Clear) -> end, mnesia:transaction(F). -normalize(A) -> jlib:nodeprep(iolist_to_binary(A)). - -normalize_spec({A, B}) -> {A, normalize(B)}; -normalize_spec({A, B, C}) -> - {A, normalize(B), normalize(C)}; -normalize_spec(all) -> all; -normalize_spec(none) -> none. - --spec match_rule(global | binary(), atom(), jid() | ljid()) -> any(). - -match_rule(global, Rule, JID) -> - case Rule of - all -> allow; - none -> deny; - _ -> - case ejabberd_config:get_global_option( - {access, Rule, global}, fun(V) -> V end) - of - undefined -> deny; - GACLs -> match_acls(GACLs, JID, global) - end - end; -match_rule(Host, Rule, JID) -> - case Rule of - all -> allow; - none -> deny; - _ -> - case ejabberd_config:get_global_option( - {access, Rule, global}, fun(V) -> V end) - of - undefined -> - case ejabberd_config:get_global_option( - {access, Rule, Host}, fun(V) -> V end) - of - undefined -> deny; - ACLs -> match_acls(ACLs, JID, Host) - end; - GACLs -> - case ejabberd_config:get_global_option( - {access, Rule, Host}, fun(V) -> V end) - of - undefined -> match_acls(GACLs, JID, Host); - ACLs -> - case lists:reverse(GACLs) of - [{allow, all} | Rest] -> - match_acls(lists:reverse(Rest) ++ - ACLs ++ [{allow, all}], - JID, Host); - _ -> match_acls(GACLs ++ ACLs, JID, Host) - end - end - end +-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([{Access, ACL} | ACLs], JID, Host) -> +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(), binary()) -> boolean(). +-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) -> - case ACL of - all -> true; - none -> false; - _ -> - {User, Server, Resource} = jlib:jid_tolower(JID), - lists:any(fun (#acl{aclspec = Spec}) -> - case Spec of - all -> true; - {user, U} -> - U == User andalso - (Host == Server orelse - Host == global andalso - lists:member(Server, ?MYHOSTS)); - {user, U, S} -> U == User andalso S == Server; - {server, S} -> S == Server; - {resource, R} -> R == Resource; - {user_regexp, UR} -> - (Host == Server orelse - Host == global andalso - lists:member(Server, ?MYHOSTS)) - andalso is_regexp_match(User, UR); - {shared_group, G} -> - Mod = loaded_shared_roster_module(Host), - Mod:is_user_in_group({User, Server}, G, Host); - {shared_group, G, H} -> - Mod = loaded_shared_roster_module(H), - Mod:is_user_in_group({User, Server}, G, H); - {user_regexp, UR, S} -> - S == Server 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} -> - (Host == Server orelse - Host == global andalso - lists:member(Server, ?MYHOSTS)) - andalso is_glob_match(User, UR); - {user_glob, UR, S} -> - S == Server 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, global}) ++ - ets:lookup(acl, {ACL, Host})) - end. + {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 @@ -286,34 +366,115 @@ 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. -update_table() -> - Fields = record_info(fields, acl), - case mnesia:table_info(acl, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - acl, Fields, bag, - fun(#acl{aclspec = Spec}) when is_tuple(Spec) -> - element(2, Spec); - (_) -> - '$next' - end, - fun(#acl{aclname = {ACLName, Host}, - aclspec = Spec} = R) -> - NewHost = if Host == global -> - Host; - true -> - iolist_to_binary(Host) - end, - R#acl{aclname = {ACLName, NewHost}, - aclspec = normalize_spec(Spec)} - end); - _ -> - ?INFO_MSG("Recreating acl table", []), - mnesia:transform_table(acl, ignore, Fields) +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]. |