diff --git a/ChangeLog b/ChangeLog index f92551ad1..14ecfa565 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,29 @@ 2008-12-23 Badlop + * src/acl.erl: New ACL: shared_group (thanks to Maxim Ryazanov) + * doc/guide.tex: Likewise + + * src/mod_shared_roster.erl: Push new group members when + registered or manually added to group: EJAB-730 EJAB-731 EJAB-732 + EJAB-767 EJAB-794. When user is added to group, push it to other + members, and other members to it. When user is removed from group, + push deletion to other members, and other members to it. When user + is registered, push him to members of group @all@. When user is + deleted, push deletion to members of group @all@. Document several + functions in mod_shared_roster. + + * src/ejabberd_auth.erl: Rename hook user_registered to + register_user, for name consistency with the widely used hook + remove_user. Run hook register_user in ejabberd_auth, so it's run + when account is created with any method. Run hook remove_user in + ejabberd_auth, so it's run when account is deleted with any + method. + * src/ejabberd_auth_internal.erl: Likewise + * src/ejabberd_auth_ldap.erl: Likewise + * src/ejabberd_auth_odbc.erl: Likewise + * src/ejabberd_auth_pam.erl: Likewise + * src/mod_register.erl: Likewise + * src/jlib.erl: Implementation of XEP-0059 Result Set Management (thanks to Eric Cestari)(EJAB-807) * src/jlib.hrl: Likewise diff --git a/doc/guide.tex b/doc/guide.tex index 0ca65220b..9501c0cc5 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -1239,6 +1239,14 @@ declarations of ACLs in the configuration file have the following syntax: \begin{verbatim} {acl, mucklres, {resource, "muckl"}}. \end{verbatim} +\titem{\{shared\_group, \}} Matches any member of a Shared Roster Group with name \term{} in the virtual host. Example: +\begin{verbatim} +{acl, techgroupmembers, {shared_group, "techteam"}}. +\end{verbatim} +\titem{\{shared\_group, , \}} Matches any member of a Shared Roster Group with name \term{} in the virtual host \term{}. Example: +\begin{verbatim} +{acl, techgroupmembers, {shared_group, "techteam", "example.org"}}. +\end{verbatim} \titem{\{user\_regexp, \}} Matches any local user with a name that matches \term{} on local virtual hosts. Example: \begin{verbatim} diff --git a/src/acl.erl b/src/acl.erl index c135c254b..4088c856d 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -180,6 +180,10 @@ match_acl(ACL, JID, Host) -> ((Host == global) andalso lists:member(Server, ?MYHOSTS))) andalso is_regexp_match(User, UR); + {shared_group, G} -> + mod_shared_roster:is_user_in_group({User, Server}, G, Host); + {shared_group, G, H} -> + mod_shared_roster:is_user_in_group({User, Server}, G, H); {user_regexp, UR, S} -> (S == Server) andalso is_regexp_match(User, UR); diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index db37e26ac..c69dda602 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -139,6 +139,7 @@ set_password(User, Server, Password) -> Res end, {error, not_allowed}, auth_modules(Server)). +%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, not_allowed} try_register(_User, _Server, "") -> %% We do not allow empty password {error, not_allowed}; @@ -149,12 +150,19 @@ try_register(User, Server, Password) -> false -> case lists:member(jlib:nameprep(Server), ?MYHOSTS) of true -> - lists:foldl( + Res = lists:foldl( fun(_M, {atomic, ok} = Res) -> Res; (M, _) -> M:try_register(User, Server, Password) - end, {error, not_allowed}, auth_modules(Server)); + end, {error, not_allowed}, auth_modules(Server)), + case Res of + {atomic, ok} -> + ejabberd_hooks:run(register_user, Server, + [User, Server]), + {atomic, ok}; + _ -> Res + end; false -> {error, not_allowed} end @@ -251,17 +259,37 @@ is_user_exists_in_other_modules(Module, User, Server) -> M:is_user_exists(User, Server) end, auth_modules(Server)--[Module]). +%% @spec (User, Server) -> ok | error | {error, not_allowed} +%% Remove user. +%% Note: it may return ok even if there was some problem removing the user. remove_user(User, Server) -> - lists:foreach( + R = lists:foreach( fun(M) -> M:remove_user(User, Server) - end, auth_modules(Server)). + end, auth_modules(Server)), + case R of + ok -> ejabberd_hooks:run(remove_user, jlib:nameprep(Server), [User, Server]); + _ -> none + end, + R. +%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request | error +%% Try to remove user if the provided password is correct. +%% The removal is attempted in each auth method provided: +%% when one returns 'ok' the loop stops; +%% if no method returns 'ok' then it returns the error message indicated by the last method attempted. remove_user(User, Server, Password) -> - lists:foreach( - fun(M) -> + R = lists:foldl( + fun(_M, ok = Res) -> + Res; + (M, _) -> M:remove_user(User, Server, Password) - end, auth_modules(Server)). + end, error, auth_modules(Server)), + case R of + ok -> ejabberd_hooks:run(remove_user, jlib:nameprep(Server), [User, Server]); + _ -> none + end, + R. %%%---------------------------------------------------------------------- diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index 7d6a8e8f8..c7982c382 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -112,6 +112,7 @@ set_password(User, Server, Password) -> ok end. +%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} | {aborted, Reason} try_register(User, Server, Password) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), @@ -237,6 +238,9 @@ is_user_exists(User, Server) -> false end. +%% @spec (User, Server) -> ok +%% Remove user. +%% Note: it returns ok even if there was some problem removing the user. remove_user(User, Server) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), @@ -245,8 +249,10 @@ remove_user(User, Server) -> mnesia:delete({passwd, US}) end, mnesia:transaction(F), - ejabberd_hooks:run(remove_user, LServer, [User, Server]). + ok. +%% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request +%% Remove user if the provided password is correct. remove_user(User, Server, Password) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), @@ -264,7 +270,6 @@ remove_user(User, Server, Password) -> end, case mnesia:transaction(F) of {atomic, ok} -> - ejabberd_hooks:run(remove_user, LServer, [User, Server]), ok; {atomic, Res} -> Res; diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 961faec56..df21f62d0 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -153,6 +153,7 @@ check_password(User, Server, Password, _StreamID, _Digest) -> set_password(_User, _Server, _Password) -> {error, not_allowed}. +%% @spec (User, Server, Password) -> {error, not_allowed} try_register(_User, _Server, _Password) -> {error, not_allowed}. diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index 6f73e6a9c..ee7603fef 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -114,6 +114,7 @@ set_password(User, Server, Password) -> end. +%% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} try_register(User, Server, Password) -> case jlib:nodeprep(User) of error -> @@ -218,6 +219,9 @@ is_user_exists(User, Server) -> end end. +%% @spec (User, Server) -> ok | error +%% Remove user. +%% Note: it may return ok even if there was some problem removing the user. remove_user(User, Server) -> case jlib:nodeprep(User) of error -> @@ -226,10 +230,11 @@ remove_user(User, Server) -> Username = ejabberd_odbc:escape(LUser), LServer = jlib:nameprep(Server), catch odbc_queries:del_user(LServer, Username), - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), - [User, Server]) + ok end. +%% @spec (User, Server, Password) -> ok | error | not_exists | not_allowed +%% Remove user if the provided password is correct. remove_user(User, Server, Password) -> case jlib:nodeprep(User) of error -> @@ -243,8 +248,6 @@ remove_user(User, Server, Password) -> LServer, Username, Pass), case Result of {selected, ["password"], [{Password}]} -> - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), - [User, Server]), ok; {selected, ["password"], []} -> not_exists; diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index f26296d33..5f67bedba 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -91,7 +91,7 @@ remove_user(_User, _Server) -> {error, not_allowed}. remove_user(_User, _Server, _Password) -> - {error, not_allowed}. + not_allowed. plain_password_required() -> true. diff --git a/src/mod_register.erl b/src/mod_register.erl index 7a4eca79f..09867fdc7 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -223,8 +223,6 @@ try_register(User, Server, Password, Source, Lang) -> true -> case ejabberd_auth:try_register(User, Server, Password) of {atomic, ok} -> - ejabberd_hooks:run(user_registered, Server, - [User, Server]), send_welcome_message(JID), send_registration_notifications(JID), ok; diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index cbce3102a..d62124d1e 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -37,7 +37,8 @@ process_item/2, in_subscription/6, out_subscription/4, - user_registered/2, + register_user/2, + remove_user/2, list_groups/1, create_group/2, create_group/3, @@ -46,6 +47,7 @@ set_group_opts/3, get_group_users/2, get_group_explicit_users/2, + is_user_in_group/3, add_user_to_group/3, remove_user_from_group/3]). @@ -83,8 +85,10 @@ start(Host, _Opts) -> ?MODULE, get_jid_info, 70), ejabberd_hooks:add(roster_process_item, Host, ?MODULE, process_item, 50), - ejabberd_hooks:add(user_registered, Host, - ?MODULE, user_registered, 50). + ejabberd_hooks:add(register_user, Host, + ?MODULE, register_user, 50), + ejabberd_hooks:add(remove_user, Host, + ?MODULE, remove_user, 50). %%ejabberd_hooks:add(remove_user, Host, %% ?MODULE, remove_user, 50), @@ -105,8 +109,10 @@ stop(Host) -> ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, ?MODULE, process_item, 50), - ejabberd_hooks:delete(user_registered, Host, - ?MODULE, user_registered, 50). + ejabberd_hooks:delete(register_user, Host, + ?MODULE, register_user, 50), + ejabberd_hooks:delete(remove_user, Host, + ?MODULE, remove_user, 50). %%ejabberd_hooks:delete(remove_user, Host, %% ?MODULE, remove_user, 50), @@ -424,6 +430,7 @@ get_group_users(_User, Host, Group, GroupOpts) -> [] end ++ get_group_explicit_users(Host, Group). +%% @spec (Host::string(), Group::string()) -> [{User::string(), Server::string()}] get_group_explicit_users(Host, Group) -> Read = (catch mnesia:dirty_index_read( sr_user, @@ -439,6 +446,7 @@ get_group_explicit_users(Host, Group) -> get_group_name(Host, Group) -> get_group_opt(Host, Group, name, Group). +%% Get list of names of groups that have @all@ in the memberlist get_special_users_groups(Host) -> lists:filter( fun(Group) -> @@ -446,6 +454,8 @@ get_special_users_groups(Host) -> end, list_groups(Host)). +%% Given two lists of groupnames and their options, +%% return the list of displayed groups to the second list displayed_groups(GroupsOpts, SelectedGroupsOpts) -> DisplayedGroups = lists:usort( @@ -457,6 +467,9 @@ displayed_groups(GroupsOpts, SelectedGroupsOpts) -> [G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))]. +%% Given a list of group names with options, +%% for those that have @all@ in memberlist, +%% get the list of groups displayed get_special_displayed_groups(GroupsOpts) -> Groups = lists:filter( fun({_Group, Opts}) -> @@ -464,6 +477,9 @@ get_special_displayed_groups(GroupsOpts) -> end, GroupsOpts), displayed_groups(GroupsOpts, Groups). +%% Given a username and server, and a list of group names with options, +%% for the list of groups of that server that user is member +%% get the list of groups displayed get_user_displayed_groups(LUser, LServer, GroupsOpts) -> Groups = case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of Rs when is_list(Rs) -> @@ -474,6 +490,7 @@ get_user_displayed_groups(LUser, LServer, GroupsOpts) -> end, displayed_groups(GroupsOpts, Groups). +%% @doc Get the list of groups that are displayed to this user get_user_displayed_groups(US) -> Host = element(2, US), DisplayedGroups1 = @@ -496,22 +513,60 @@ is_user_in_group({_U, S} = US, Group, Host) -> _ -> true end. + +%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} add_user_to_group(Host, US, Group) -> + {LUser, LServer} = US, + %% Push this new user to members of groups where this group is displayed + push_user_to_displayed(LUser, LServer, Group, both), + %% Push members of groups that are displayed to this group + push_displayed_to_user(LUser, LServer, Group, Host, both), R = #sr_user{us = US, group_host = {Group, Host}}, F = fun() -> mnesia:write(R) end, mnesia:transaction(F). +push_displayed_to_user(LUser, LServer, Group, Host, Subscription) -> + GroupsOpts = groups_with_opts(LServer), + GroupOpts = proplists:get_value(Group, GroupsOpts, []), + DisplayedGroups = proplists:get_value(displayed_groups, GroupOpts, []), + [push_members_to_user(LUser, LServer, DGroup, Host, Subscription) || DGroup <- DisplayedGroups]. + remove_user_from_group(Host, US, Group) -> GroupHost = {Group, Host}, R = #sr_user{us = US, group_host = GroupHost}, F = fun() -> mnesia:delete_object(R) end, - mnesia:transaction(F). + Result = mnesia:transaction(F), + {LUser, LServer} = US, + %% Push removal of the old user to members of groups where the group that this user was members was displayed + push_user_to_displayed(LUser, LServer, Group, remove), + %% Push removal of members of groups that where displayed to the group which this user has left + push_displayed_to_user(LUser, LServer, Group, Host, remove), + Result. -user_registered(User, Server) -> +push_members_to_user(LUser, LServer, Group, Host, Subscription) -> + GroupsOpts = groups_with_opts(LServer), + GroupOpts = proplists:get_value(Group, GroupsOpts, []), + GroupName = proplists:get_value(name, GroupOpts, Group), + Members = get_group_users(Host, Group), + lists:foreach( + fun({U, S}) -> + push_roster_item(LUser, LServer, U, S, GroupName, Subscription) + end, Members). + +register_user(User, Server) -> + %% Get list of groups where this user is member + Groups = get_user_groups({User, Server}), + %% Push this user to members of groups where is displayed a group which this user is member + [push_user_to_displayed(User, Server, Group, both) || Group <- Groups]. + +remove_user(User, Server) -> + push_user_to_members(User, Server, remove). + +push_user_to_members(User, Server, Subscription) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), GroupsOpts = groups_with_opts(LServer), @@ -523,17 +578,31 @@ user_registered(User, Server) -> GroupName = proplists:get_value(name, GroupOpts, Group), lists:foreach( fun({U, S}) -> - Item = #roster{usj = {U, S, {LUser, LServer, ""}}, - us = {U, S}, - jid = {LUser, LServer, ""}, - name = "", - subscription = both, - ask = none, - groups = [GroupName]}, - push_item(U, S, jlib:make_jid("", S, ""), Item) + push_roster_item(U, S, LUser, LServer, GroupName, Subscription) end, get_group_users(LUser, LServer, Group, GroupOpts)) end, lists:usort(SpecialGroups++UserGroups)). +push_user_to_displayed(LUser, LServer, Group, Subscription) -> + GroupsOpts = groups_with_opts(LServer), + GroupOpts = proplists:get_value(Group, GroupsOpts, []), + GroupName = proplists:get_value(name, GroupOpts, Group), + DisplayedToGroupsOpts = displayed_to_groups(Group, LServer), + [push_user_to_group(LUser, LServer, GroupD, GroupName, Subscription) || {GroupD, _Opts} <- DisplayedToGroupsOpts]. + +push_user_to_group(LUser, LServer, Group, GroupName, Subscription) -> + lists:foreach( + fun({U, S}) -> + push_roster_item(U, S, LUser, LServer, GroupName, Subscription) + end, get_group_users(LServer, Group)). + +%% Get list of groups to which this group is displayed +displayed_to_groups(GroupName, LServer) -> + GroupsOpts = groups_with_opts(LServer), + lists:filter( + fun({_Group, Opts}) -> + lists:member(GroupName, proplists:get_value(displayed_groups, Opts, [])) + end, GroupsOpts). + push_item(_User, _Server, _From, none) -> ok; push_item(User, Server, From, Item) -> @@ -558,6 +627,16 @@ push_item(User, Server, From, Item) -> ejabberd_router:route(JID, JID, Stanza) end, ejabberd_sm:get_user_resources(User, Server)). +push_roster_item(User, Server, ContactU, ContactS, GroupName, Subscription) -> + Item = #roster{usj = {User, Server, {ContactU, ContactS, ""}}, + us = {User, Server}, + jid = {ContactU, ContactS, ""}, + name = "", + subscription = Subscription, + ask = none, + groups = [GroupName]}, + push_item(User, Server, jlib:make_jid("", Server, ""), Item). + item_to_xml(Item) -> Attrs1 = [{"jid", jlib:jid_to_string(Item#roster.jid)}], Attrs2 = case Item#roster.name of