From 6b126171da22dbf8b49fd10ac4248be588a4d0f2 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Fri, 26 Feb 2016 17:27:12 +0300 Subject: [PATCH] Improve LDAP shared roster support (EJAB-1480) --- src/mod_shared_roster_ldap.erl | 1045 ++++++++++++++++++++------------ 1 file changed, 668 insertions(+), 377 deletions(-) diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index a4ac65c10..3275394be 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -3,6 +3,7 @@ %%% Author : Realloc %%% Marcin Owsiany %%% Evgeniy Khramtsov +%%% Contributor : Mike Kaganski %%% Description : LDAP shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin %%% @@ -26,8 +27,6 @@ %%%------------------------------------------------------------------- -module(mod_shared_roster_ldap). --behaviour(ejabberd_config). - -behaviour(gen_server). -behaviour(gen_mod). @@ -44,19 +43,15 @@ out_subscription/4, mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). --include("logger.hrl"). -include("jlib.hrl"). -include("mod_roster.hrl"). - -include("eldap.hrl"). +-define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). --define(CACHE_SIZE, 1000). - --define(USER_CACHE_VALIDITY, 300). - --define(GROUP_CACHE_VALIDITY, 300). - --define(LDAP_SEARCH_TIMEOUT, 5). +-define(CACHE_SIZE, 1). +-define(CACHE_VALIDITY, 300). %% in seconds +-define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds +-define(INVALID_SETTING_MSG, "~s is not properly set! ~s will not function."). -record(state, {host = <<"">> :: binary(), @@ -74,7 +69,6 @@ group_attr = <<"">> :: binary(), group_desc = <<"">> :: binary(), user_desc = <<"">> :: binary(), - user_uid = <<"">> :: binary(), uid_format = <<"">> :: binary(), uid_format_re = <<"">> :: binary(), filter = <<"">> :: binary(), @@ -82,25 +76,51 @@ rfilter = <<"">> :: binary(), gfilter = <<"">> :: binary(), auth_check = true :: boolean(), - user_cache_size = ?CACHE_SIZE :: non_neg_integer(), - group_cache_size = ?CACHE_SIZE :: non_neg_integer(), - user_cache_validity = ?USER_CACHE_VALIDITY :: non_neg_integer(), - group_cache_validity = ?GROUP_CACHE_VALIDITY :: non_neg_integer()}). + %% Group data parameters + group_base = <<"">> :: binary(), + %% - Subgroup of roster filter + %% This filter defines which groups are displayed in the shared roster + %% Valid values are 'all' or "LDAP filter string" or "LDAP filter string containing %g" + shgfilter = <<"">> :: binary(), + shg_attr = <<"">> :: binary(), + %% - Subgroup of group filter + group_is_dn = true :: boolean(), + member_attr = <<"">> :: binary(), + %% User data parameters + member_selection_mode = memberattr_dn :: memberattr_normal | memberattr_dn | + group_children, + %% Algorithm control parameters + subscribe_all = false :: binary(), + roster_cache_size = ?CACHE_SIZE :: non_neg_integer(), + roster_cache_validity = ?CACHE_VALIDITY :: non_neg_integer()}). +%% If #state.member_selection_mode is memberattr_normal or memberattr_dn, +%% then members is list of member_attr values; +%% if #state.member_selection_mode is group_children, +%% then members is dn of the group (to make it possible to search for its subtree) -record(group_info, {desc, members}). +-record(user_info, {us, name}). + +-record(shared_roster_item, {us, name, groups}). + +% Groups visible to this group +% grp may be atom 'all' or a group name string. +% shgrps is a list containing one or more grp +-record(shg_data, {grp, shgrps}). + %%==================================================================== %% API %%==================================================================== start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). + [Host, Opts], []). start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - permanent, 1000, worker, [?MODULE]}, + permanent, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> @@ -112,109 +132,92 @@ stop(Host) -> %% Hooks %%-------------------------------------------------------------------- get_user_roster(Items, {U, S} = US) -> - SRUsers = get_user_to_groups_map(US, true), - {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, - SRUsers1) -> - {_, _, {U1, S1, _}} = - Item#roster.usj, - US1 = {U1, S1}, - case dict:find(US1, - SRUsers1) - of - {ok, _GroupNames} -> - {Item#roster{subscription - = - both, - ask = - none}, - dict:erase(US1, - SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), + {ok, State} = eldap_utils:get_state(S, ?MODULE), + SRUsers = get_shared_roster(State, US), + %%?ERROR_MSG("XXXXXX get_user_roster: SRUsers=~p", [SRUsers]), + %% If partially subscribed users are also in shared roster, + %% show them as totally subscribed: + {NewItems1, SRUsersRest} = lists:mapfoldl( + fun(Item, SRUsers1) -> + {_, _, {U1, S1, _}} = Item#roster.usj, + US1 = {U1, S1}, + case lists:keytake(US1, #shared_roster_item.us, SRUsers1) of + %%case dict:find(US1, SRUsers1) of + {value, _, SRUsers2} -> {Item#roster{subscription = both, ask = none}, SRUsers2}; + %%{ok, _GroupNames} -> {Item#roster{subscription = both, ask = none}, dict:erase(US1, SRUsers1)}; + error -> {Item, SRUsers1} + end + end, + SRUsers, Items), + %% Export items in roster format: SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_user_name(U1, S1), subscription = both, - ask = none, groups = GroupNames} - || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], + us = US, + jid = {U1, S1, <<"">>}, + name = Name, + subscription = both, + ask = none, + groups = Groups} || + #shared_roster_item{us = {U1, S1}, name = Name, groups = Groups} <- SRUsersRest], + %% SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, + %% us = US, jid = {U1, S1, <<"">>}, + %% name = get_user_name(U1, S1), subscription = both, + %% ask = none, groups = GroupNames} + %% || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. %% This function in use to rewrite the roster entries when moving or renaming %% them in the user contact list. process_item(RosterItem, _Host) -> - USFrom = RosterItem#roster.us, - {User, Server, _Resource} = RosterItem#roster.jid, - USTo = {User, Server}, - Map = get_user_to_groups_map(USFrom, false), - case dict:find(USTo, Map) of - error -> RosterItem; - {ok, []} -> RosterItem; - {ok, GroupNames} - when RosterItem#roster.subscription == remove -> - RosterItem#roster{subscription = both, ask = none, - groups = GroupNames}; - _ -> RosterItem#roster{subscription = both, ask = none} + {ok, State} = eldap_utils:get_state(_Host, ?MODULE), + {User,Server,_Resource} = RosterItem#roster.jid, + USTo = {User,Server}, + SR = get_shared_roster(State, RosterItem#roster.us), + case lists:keysearch(USTo, #shared_roster_item.us, SR) of + false -> + RosterItem; + {value, #shared_roster_item{groups = Groups}} when RosterItem#roster.subscription == remove -> + %% Roster item cannot be removed: + %% We simply reset the original groups: + RosterItem#roster{subscription = both, ask = none, + groups=Groups}; + _ -> + RosterItem#roster{subscription = both, ask = none} end. get_subscription_lists({F, T}, User, Server) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), - US = {LUser, LServer}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - SRJIDs = [{U1, S1, <<"">>} || {U1, S1} <- SRUsers], + U = jlib:nodeprep(User), + S = jlib:nameprep(Server), + {ok, State} = eldap_utils:get_state(S, ?MODULE), + SRJIDs = get_presense_subscribers(State, {U, S}), +%?INFO_MSG("SRJIDs: ~p", [SRJIDs]), {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}. -get_jid_info({Subscription, Groups}, User, Server, - JID) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), - US = {LUser, LServer}, - {U1, S1, _} = jid:tolower(JID), +get_jid_info({Subscription, Groups}, User, Server, JID) -> + {ok, State} = eldap_utils:get_state(Server, ?MODULE), + {U1, S1, _} = jlib:jid_tolower(JID), US1 = {U1, S1}, - SRUsers = get_user_to_groups_map(US, false), - case dict:find(US1, SRUsers) of - {ok, GroupNames} -> - NewGroups = if Groups == [] -> GroupNames; - true -> Groups - end, - {both, NewGroups}; - error -> {Subscription, Groups} + SR = get_shared_roster(State, {User, Server}), + case lists:keysearch(US1, #shared_roster_item.us, SR) of + false -> {Subscription, Groups}; + {value, #shared_roster_item{groups = GroupNames}} when Groups == [] -> {both, GroupNames}; + _ -> {both, Groups} end. -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +in_subscription(Acc, User, Server, JID, Type, _Reason) -> process_subscription(in, User, Server, JID, Type, Acc). out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). + process_subscription(out, User, Server, JID, Type, false). -process_subscription(Direction, User, Server, JID, - _Type, Acc) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), - US = {LUser, LServer}, - {U1, S1, _} = - jid:tolower(jid:remove_resource(JID)), +process_subscription(Direction, User, Server, JID, _Type, Acc) -> + {ok, State} = eldap_utils:get_state(Server, ?MODULE), + {U1, S1, _} = jlib:jid_tolower(JID), US1 = {U1, S1}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - case lists:member(US1, SRUsers) of - true -> - case Direction of - in -> {stop, false}; - out -> stop - end; - false -> Acc + SR = get_shared_roster(State, {User, Server}), + case lists:keysearch(US1, #shared_roster_item.us, SR) of + false -> Acc; + _ when Direction == in -> {stop, false}; + _ -> stop end. %%==================================================================== @@ -222,267 +225,509 @@ process_subscription(Direction, User, Server, JID, %%==================================================================== init([Host, Opts]) -> State = parse_options(Host, Opts), - cache_tab:new(shared_roster_ldap_user, - [{max_size, State#state.user_cache_size}, {lru, false}, - {life_time, State#state.user_cache_validity}]), - cache_tab:new(shared_roster_ldap_group, - [{max_size, State#state.group_cache_size}, {lru, false}, - {life_time, State#state.group_cache_validity}]), - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 70), + if + State#state.roster_cache_size > 0 -> + cache_tab:new(shared_roster_ldap_sr, + [{max_size, State#state.roster_cache_size}, + {lru, false}, % We don't need LRU algorithm + {life_time, State#state.roster_cache_validity}]); + true -> + false + end, + ejabberd_hooks:add(roster_get, Host, + ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), + ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), + ?MODULE, out_subscription, 30), ejabberd_hooks:add(roster_get_subscription_lists, Host, - ?MODULE, get_subscription_lists, 70), - ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, - get_jid_info, 70), - ejabberd_hooks:add(roster_process_item, Host, ?MODULE, - process_item, 50), + ?MODULE, get_subscription_lists, 70), + ejabberd_hooks:add(roster_get_jid_info, Host, + ?MODULE, get_jid_info, 70), + ejabberd_hooks:add(roster_process_item, Host, + ?MODULE, process_item, 50), eldap_pool:start_link(State#state.eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), {ok, State}. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; + handle_call(_Request, _From, State) -> {reply, {error, badarg}, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(_Msg, State) -> + {noreply, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info(_Info, State) -> + {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 70), + ejabberd_hooks:delete(roster_get, Host, + ?MODULE, get_user_roster, 70), ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), + ?MODULE, in_subscription, 30), ejabberd_hooks:delete(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:delete(roster_get_subscription_lists, - Host, ?MODULE, get_subscription_lists, 70), + ?MODULE, out_subscription, 30), + ejabberd_hooks:delete(roster_get_subscription_lists, Host, + ?MODULE, get_subscription_lists, 70), ejabberd_hooks:delete(roster_get_jid_info, Host, - ?MODULE, get_jid_info, 70), + ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, - ?MODULE, process_item, 50). + ?MODULE, process_item, 50). -code_change(_OldVsn, State, _Extra) -> {ok, State}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -get_user_to_groups_map({_, Server} = US, SkipUS) -> - DisplayedGroups = get_user_displayed_groups(US), - lists:foldl(fun (Group, Dict1) -> - GroupName = get_group_name(Server, Group), - lists:foldl(fun (Contact, Dict) -> - if SkipUS, Contact == US -> Dict; - true -> - dict:append(Contact, - GroupName, Dict) - end - end, - Dict1, get_group_users(Server, Group)) - end, - dict:new(), DisplayedGroups). - -eldap_search(State, FilterParseArgs, AttributesList) -> - case apply(eldap_filter, parse, FilterParseArgs) of - {ok, EldapFilter} -> - case eldap_pool:search(State#state.eldap_id, - [{base, State#state.base}, - {filter, EldapFilter}, - {timeout, ?LDAP_SEARCH_TIMEOUT}, - {deref_aliases, State#state.deref_aliases}, - {attributes, AttributesList}]) - of - #eldap_search_result{entries = Es} -> - %% A result with entries. Return their list. - Es; - _ -> - %% Something else. Pretend we got no results. - [] - end; - _ -> - %% Filter parsing failed. Pretend we got no results. - [] +do_eldap_search(PoolName, Opts) -> + case eldap_pool:search(PoolName, Opts) of + #eldap_search_result{entries = Es} -> + %% A result with entries. Return their list. + Es; + Err -> + %% Something else. Pretend we got no results. + ?ERROR_MSG("Error searching: ~p ~p", [Err, Opts]), + [] end. -get_user_displayed_groups({User, Host}) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - GroupAttr = State#state.group_attr, - Entries = eldap_search(State, - [eldap_filter:do_sub(State#state.rfilter, - [{<<"%u">>, User}])], - [GroupAttr]), - Reply = lists:flatmap(fun (#eldap_entry{attributes = - Attrs}) -> - case Attrs of - [{GroupAttr, ValuesList}] -> ValuesList; - _ -> [] - end - end, - Entries), - lists:usort(Reply). +%% Pass given Filter or FilterTemplate and SubstList to eldap_filter:parse, +%% and if successful, run LDAP search on the whole subtree of Base, using +%% resulting filter, retrieving given AttributesList. Return the result entries. +%% On any error, print an error message and return an empty list of results. +eldap_search(State, Base, EldapFilter, AttributesList) when is_tuple(EldapFilter) -> + do_eldap_search(State#state.eldap_id, + [{base, Base}, +%% {scope, wholeSubtree} %% This is the default + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, AttributesList}]); +%% Filter is string +eldap_search(State, Base, Filter, AttributesList) -> + eldap_search(State, Base, Filter, [], AttributesList). -get_group_users(Host, Group) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{members = Members}} - when Members /= undefined -> - Members; - _ -> [] +eldap_search(State, Base, FilterTemplate, SubstList, AttributesList) -> + case apply(eldap_filter, parse, [eldap_filter:do_sub(FilterTemplate, SubstList)]) of + {ok, EldapFilter} -> + %% Filter parsing succeeded + eldap_search(State, Base, EldapFilter, AttributesList); + Err -> + %% Filter parsing failed. Pretend we got no results. + ?ERROR_MSG("Error parsing filter: ~p", [Err]), + [] end. -get_group_name(Host, Group) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{desc = GroupName}} - when GroupName /= undefined -> - GroupName; - _ -> Group +%% The same as above, but gets the Attributes for the specified DN. +%% Note that this function doesn't honor the State's base DN; +%% TODO: fix this (create a custom check?) +eldap_search_dn(State, DN, EldapFilter, AttributesList) when is_tuple(EldapFilter) -> + do_eldap_search(State#state.eldap_id, + [{scope, baseObject}, + {base, DN}, + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, AttributesList}]); +%% Filter is string. +eldap_search_dn(State, DN, Filter, AttributesList) -> + case eldap_filter:parse(Filter) of + {ok, EldapFilter} -> + %% Filter parsing succeeded + eldap_search_dn(State, DN, EldapFilter, AttributesList); + Err -> + %% Filter parsing failed. Pretend we got no results. + ?ERROR_MSG("Error parsing filter: ~p", [Err]), + [] end. -get_user_name(User, Host) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_user, - {User, Host}, - fun () -> search_user_name(State, User) end) - of - {ok, UserName} -> UserName; - error -> User +intersection(L1,L2) -> lists:filter(fun(X) -> lists:member(X,L1) end, L2). + +filter_roster(Roster, all) -> Roster; +filter_roster(_, []) -> []; +filter_roster(Roster, IncludeGroups) when is_list(IncludeGroups) -> + lists:foldl( + fun(RosterItem, Acc) -> + case intersection(IncludeGroups, RosterItem#shared_roster_item.groups) of + [] -> Acc; + CommonGroups -> [RosterItem#shared_roster_item{groups=CommonGroups} | Acc] + end + end, + [], Roster). + +get_user_visible_groups(UserGroups, VisibilityMap) -> + lists:foldl( + fun(Group, Acc) -> + case (lists:keysearch(Group, #shg_data.grp, VisibilityMap)) of + {value, #shg_data{shgrps=Gs}} when is_list(Gs) -> Gs ++ Acc; + _ -> Acc + end + end, + UserGroups, UserGroups). + +%% Returns [#shared_roster_item]; +%% Removes the US from returned data +%% If State#state.user_groups_only is 'true', then it removes all users that are not in US's groups, +%% and also removes the groups from the users that the US is not member of. +get_shared_roster(State, {_, Server} = US) -> + case (catch get_full_roster(State, Server)) of + {ok, {VisibilityMap, FullRoster}} -> + %%?ERROR_MSG("XXXXXX get_shared_roster: VMap=~p FullRoster=~p", [VisibilityMap, FullRoster]), + CommonRosterGroups = lists:foldl( + fun(_, all) -> all; + (#shg_data{grp=all, shgrps=all}, _) -> all; + (#shg_data{grp=all, shgrps=Gs}, Acc) when is_list(Gs) -> Gs ++ Acc; + (_, Acc) -> Acc + end, + [], VisibilityMap), + case lists:keytake(US, #shared_roster_item.us, FullRoster) of + false -> filter_roster(FullRoster, CommonRosterGroups); + {value, #shared_roster_item{groups=UserGroups}, Roster2} -> + VisibleGroups = case (CommonRosterGroups) of + all -> all; + CRG -> get_user_visible_groups(UserGroups, VisibilityMap) ++ CRG + end, + filter_roster(Roster2, VisibleGroups) + end; + {'EXIT', CatchData} -> ?ERROR_MSG("Error getting shared roster for user ~p: ~p", [US, CatchData]), []; + _Unexpected -> [] end. +%% 1. If user is not a member of shared roster -> no additional subscriptions +%% 2. Else if ldap_subscribe_all is set AND this user is member of a group published to all -> +%% add all registered users of this vhost +%% 3. Else add only those groups this user' groups are published to +get_presense_subscribers(State, {_, Server} = US) -> + case (catch get_full_roster(State, Server)) of + {ok, {VisibilityMap, FullRoster}} -> + case lists:keytake(US, #shared_roster_item.us, FullRoster) of + false -> []; % Case #1 + {value, #shared_roster_item{groups=UserGroups}, Roster2} -> + AllGroups = lists:usort(lists:foldl( + fun(#shared_roster_item{groups=Gs}, Acc) -> Gs ++ Acc end, + [], FullRoster)), + Fun = case (State#state.subscribe_all) of + true -> % Possible case 2 + fun(_, all) -> all; + (#shg_data{grp=all, shgrps=all}, _) -> all; + (#shg_data{grp=all, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> all + end; + (#shg_data{grp=G, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> [G | Acc] + end; + (_, Acc) -> Acc + end; + _False -> % Case 3 + fun(#shg_data{grp=all}, Acc) -> AllGroups ++ Acc; + (#shg_data{grp=G, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> [G | Acc] + end; + (_, Acc) -> Acc + end + end, + PublishTo = lists:foldl(Fun, [], VisibilityMap), + case (PublishTo) of + all -> + [{U1, S1, <<"">>} || {U1, S1} <- ejabberd_auth:get_vh_registered_users(Server)]; + Groups -> + [{U1, S1, <<"">>} || #shared_roster_item{us = {U1, S1}} <- filter_roster(Roster2, UserGroups ++ Groups)] + end + end; + {'EXIT', CatchData} -> ?ERROR_MSG("Error getting shared roster for user ~p: ~p", [US, CatchData]), []; + _Unexpected -> [] + end. + +get_full_roster(State, Server) when State#state.roster_cache_size > 0 -> + cache_tab:dirty_lookup(shared_roster_ldap_sr, + {Server}, + fun() -> search_roster_info(State, Server) end); +get_full_roster(State, Server) -> + search_roster_info(State, Server). + +search_visible_groups(State, _) when State#state.shgfilter == all -> + [{all, all}]; +search_visible_groups(State, _) when State#state.shgfilter == none -> + [{all, none}]; +search_visible_groups(State, Groups) -> + case (string:str(State#state.shgfilter, "%g")) of + 0 -> [{all, search_group_visible_groups(State, "")}]; + _ -> lists:map( + fun(Group) -> {Group, search_group_visible_groups(State, Group)} end, + Groups) + end. + +search_group_visible_groups(State, Group) -> + Entries = eldap_search(State, State#state.group_base, State#state.shgfilter, [{<<"%g">>, Group}], [State#state.shg_attr]), + lists:usort(lists:flatmap( + fun(#eldap_entry{attributes = Attrs}) -> + case Attrs of + [{_GroupAttr, ValuesList}] -> + ValuesList; + _ -> + [] + end + end, Entries)). + +group2name(all, _) -> all; +group2name(none, _) -> none; +group2name(Group, GroupNames) -> + case (lists:keysearch(Group, 1, GroupNames)) of + {value, {_, Name}} -> Name; + _ -> false + end. + +groups2names(all, _) -> all; +groups2names(none, _) -> none; +groups2names(GroupList, GroupNames) -> + lists:foldl( + fun(G, Acc) -> + case (group2name(G, GroupNames)) of + false -> Acc; + Name -> [Name | Acc] + end + end, + [], GroupList). + +prep_vis_map(VisGroups, GroupNames) -> + lists:foldl( + fun({G, Gs}, Acc) -> + case (group2name(G, GroupNames)) of + false -> Acc; + Name -> [#shg_data{grp=Name, shgrps=groups2names(Gs, GroupNames)} | Acc] + end + end, + [], VisGroups). + +search_roster_info(State, _Host) -> + Entries = eldap_search(State, State#state.group_base, State#state.rfilter, [State#state.group_attr]), + AllGroupIds = lists:usort(lists:flatmap( + fun(#eldap_entry{attributes = Attrs}) -> + case Attrs of + [{_GroupAttr, ValuesList}] -> + ValuesList; + _ -> + [] + end + end, Entries)), + VisGroups = search_visible_groups(State, AllGroupIds), + %%?ERROR_MSG("XXXXXX search_roster_info: VisGroups=~p", [VisGroups]), + + {GroupNames, RosterItems} = case State#state.member_selection_mode of + group_children -> + {GroupNames0, UsersDict0} = lists:foldl( + fun(Group, {GrNAcc, Dict1} = Acc) -> + case search_group_info(State, Group) of + {ok, #group_info{desc = GroupName, members = GroupDN}} -> + {[{Group, GroupName} | GrNAcc], search_users_info(State, GroupDN, GroupName, Dict1)}; + _ -> Acc %% Error getting group data -> No users! + end + end, + {[], dict:new()}, AllGroupIds), + + {GroupNames0, dict:fold( + fun(#user_info{us=US, name=UserName}, Groups, AccIn) -> + [#shared_roster_item{us = US, name = UserName, groups = Groups} | AccIn] + end, + [], UsersDict0)}; + _ -> + {GroupNames1, UsersDict1} = lists:foldl( + fun(Group, {GrNAcc, Dict1} = Acc) -> + case search_group_info(State, Group) of + {ok, #group_info{desc = GroupName, members = Members}} -> + {[{Group, GroupName} | GrNAcc], lists:foldl( + fun(Member, Dict) -> dict:append(Member, GroupName, Dict) end, + Dict1, Members)}; + _ -> Acc %% Error getting group data -> No users! + end + end, + {[], dict:new()}, AllGroupIds), + + %%?ERROR_MSG("UsersDict1: ~p", [UsersDict1]), + %%?ERROR_MSG("GroupNames1: ~p", [GroupNames1]), + + {GroupNames1, dict:fold( + fun(Member, Groups, AccIn) -> + case search_user_info(State, Member) of + {ok, #user_info{us=US, name=UserName}} -> + %%?ERROR_MSG("XXXX found user: ~p ~p ~p", [UserName, Groups, US]), + [#shared_roster_item{us = US, name = UserName, groups = Groups} | AccIn]; + _ -> AccIn + end + end, + [], UsersDict1)} + end, + + VisibilityMap = prep_vis_map(VisGroups, GroupNames), + {ok, {VisibilityMap, RosterItems}}. + search_group_info(State, Group) -> - Extractor = case State#state.uid_format_re of - <<"">> -> - fun (UID) -> - catch eldap_utils:get_user_part(UID, - State#state.uid_format) - end; - _ -> - fun (UID) -> - catch get_user_part_re(UID, - State#state.uid_format_re) - end - end, - AuthChecker = case State#state.auth_check of - true -> fun ejabberd_auth:is_user_exists/2; - _ -> fun (_U, _S) -> true end - end, - Host = State#state.host, - case eldap_search(State, - [eldap_filter:do_sub(State#state.gfilter, - [{<<"%g">>, Group}])], - [State#state.group_attr, State#state.group_desc, - State#state.uid]) - of - [] -> error; - LDAPEntries -> - {GroupDesc, MembersLists} = lists:foldl(fun - (#eldap_entry{attributes = - Attrs}, - {DescAcc, JIDsAcc}) -> - case - {eldap_utils:get_ldap_attr(State#state.group_attr, - Attrs), - eldap_utils:get_ldap_attr(State#state.group_desc, - Attrs), - lists:keysearch(State#state.uid, - 1, - Attrs)} - of - {ID, Desc, - {value, - {GroupMemberAttr, - Members}}} - when ID /= <<"">>, - GroupMemberAttr - == - State#state.uid -> - JIDs = - lists:foldl(fun - ({ok, - UID}, - L) -> - PUID = - jid:nodeprep(UID), - case - PUID - of - error -> - L; - _ -> - case - AuthChecker(PUID, - Host) - of - true -> - [{PUID, - Host} - | L]; - _ -> - L - end - end; - (_, - L) -> - L - end, - [], - lists:map(Extractor, - Members)), - {Desc, - [JIDs - | JIDsAcc]}; - _ -> - {DescAcc, JIDsAcc} - end - end, - {Group, []}, LDAPEntries), - {ok, - #group_info{desc = GroupDesc, - members = lists:usort(lists:flatten(MembersLists))}} + AttList = case State#state.member_selection_mode of + group_children -> [State#state.group_desc]; + _ -> [State#state.group_desc, State#state.member_attr] + end, + SearchResult = case State#state.group_is_dn of + true -> eldap_search_dn(State, + Group, + State#state.gfilter, + AttList); + _ -> eldap_search(State, + State#state.group_base, + State#state.gfilter, + [{<<"%g">>, Group}], + AttList) + end, + case SearchResult of + [] -> + error; + LDAPEntries -> + case State#state.member_selection_mode of + group_children -> + [#eldap_entry{object_name=Name, attributes=Attrs} | _] = LDAPEntries, + {ok, #group_info{desc = eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), + members = Name}}; + _ -> + {GroupDesc, MembersLists} = lists:foldl( + fun(#eldap_entry{attributes=Attrs}, {DescAcc, MembersAcc}) -> + case {eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), + lists:keysearch(State#state.member_attr, 1, Attrs)} of + {Desc, {value, {GroupMemberAttr, Members}}} + when GroupMemberAttr == State#state.member_attr -> + {Desc, lists:usort(Members ++ MembersAcc)}; + _ -> + {DescAcc, MembersAcc} + end + end, + {Group, []}, LDAPEntries), + {ok, #group_info{desc = GroupDesc, + members = lists:usort(MembersLists)}} + end end. -search_user_name(State, User) -> - case eldap_search(State, - [eldap_filter:do_sub(State#state.ufilter, - [{<<"%u">>, User}])], - [State#state.user_desc, State#state.user_uid]) - of - [#eldap_entry{attributes = Attrs} | _] -> - case {eldap_utils:get_ldap_attr(State#state.user_uid, - Attrs), - eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} - of - {UID, Desc} when UID /= <<"">> -> {ok, Desc}; - _ -> error - end; - [] -> error +%% Takes the attributes from LDAP user search; +%% returns error or {ok, #user_info} +construct_user(State, Attrs) -> + Extractor = case State#state.uid_format_re of + <<"">> -> fun(UID) -> + catch eldap_utils:get_user_part(UID, State#state.uid_format) + end; + _ -> fun(UID) -> + catch get_user_part_re(UID, State#state.uid_format_re) + end + end, + AuthChecker = case State#state.auth_check of + true -> fun ejabberd_auth:is_user_exists/2; + _ -> fun(_U, _S) -> true end + end, + Host = State#state.host, + + case {eldap_utils:get_ldap_attr(State#state.uid, Attrs), + eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} of + {UID, Desc} when UID /= "" -> + %% By returning "" get_ldap_attr means "not found" + case Extractor(UID) of + {ok, UID1} -> + UID2 = jlib:nodeprep(UID1), + case UID2 of + error -> error; + _ -> + case AuthChecker(UID2, Host) of + true -> {ok, #user_info{us={UID2, Host}, name=Desc}}; + _ -> error + end + end; + _ -> error + end; + _ -> + error + end. + +%% This function is used when State#state.member_selection_mode is group_children +%% Returns UsersDict to which the users (#user_info) of this group are added +%%search_users_info(State, GroupInfo) -> +search_users_info(State, GroupDN, GroupName, UsersDict) -> + SearchResult = eldap_search(State, + GroupDN, + State#state.ufilter, + [State#state.user_desc, State#state.uid]), + lists:foldl( + fun(#eldap_entry{attributes=Attrs}, Dict1) -> + case construct_user(State, Attrs) of + {ok, UserInfo} -> + dict:append(UserInfo, GroupName, Dict1); + _ -> Dict1 + end + end, UsersDict, SearchResult). + +%% This function is used when State#state.member_selection_mode is either memberattr_normal or memberattr_dn +search_user_info(State, User) -> + %%?ERROR_MSG("XXX search_user_info: searching for ~p", [User]), + SearchResult = case State#state.member_selection_mode of + memberattr_dn -> eldap_search_dn(State, + User, + State#state.ufilter, + [State#state.user_desc, State#state.uid]); + memberattr_normal -> eldap_search(State, + State#state.base, + State#state.ufilter, + [{<<"%u">>, User}], + [State#state.user_desc, State#state.uid]) + end, + case SearchResult of + [#eldap_entry{attributes=Attrs}|_] -> + construct_user(State, Attrs); + [] -> + %%?ERROR_MSG("XX not found", []), + error end. %% Getting User ID part by regex pattern get_user_part_re(String, Pattern) -> case catch re:run(String, Pattern) of - {match, Captured} -> - {First, Len} = lists:nth(2, Captured), - Result = str:sub_string(String, First + 1, First + Len), - {ok, Result}; - _ -> {error, badmatch} + {match, Captured} -> + {First, Len} = lists:nth(2,Captured), + Result = string:sub_string(String, First+1, First+Len), + {ok,Result}; + _ -> {error, badmatch} end. +% select(SelectFirst, First, Second) -> +% case SelectFirst of +% true -> First; +% _ -> Second +% end. + +% prepare_filter(Opts, Name, Default, ReturnParsed) -> +% F = gen_mod:get_opt(Name, Opts, Default), +% prepare_filter(F, Name, ReturnParsed). + +% prepare_filter(F, Name, ReturnParsed) -> +% case eldap_filter:parse(F) of +% {ok, EldapFilter} -> +% case ReturnParsed of +% true -> EldapFilter; +% _ -> F +% end; +% _ -> +% ?ERROR_MSG(?INVALID_SETTING_MSG, [atom_to_list(Name), ?MODULE]), +% [] +% end. + parse_options(Host, Opts) -> Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Cfg = eldap_utils:get_config(Host, Opts), @@ -516,84 +761,114 @@ parse_options(Host, Opts) -> (false) -> false; (true) -> true end, true), - UserCacheValidity = gen_mod:get_opt( - {ldap_user_cache_validity, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?USER_CACHE_VALIDITY), - GroupCacheValidity = gen_mod:get_opt( + RosterCacheValidity = eldap_utils:get_opt( {ldap_group_cache_validity, Host}, Opts, fun(I) when is_integer(I), I>0 -> I end, - ?GROUP_CACHE_VALIDITY), - UserCacheSize = gen_mod:get_opt( - {ldap_user_cache_size, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?CACHE_SIZE), - GroupCacheSize = gen_mod:get_opt( - {ldap_group_cache_size, Host}, Opts, + ?CACHE_VALIDITY), + RosterCacheSize = eldap_utils:get_opt( + {ldap_roster_cache_size, Host}, Opts, fun(I) when is_integer(I), I>0 -> I end, ?CACHE_SIZE), - ConfigFilter = gen_mod:get_opt({ldap_filter, Host}, Opts, + ConfigFilter = eldap_utils:get_opt({ldap_filter, Host}, Opts, fun check_filter/1, <<"">>), - ConfigUserFilter = gen_mod:get_opt({ldap_ufilter, Host}, Opts, + ConfigUserFilter = eldap_utils:get_opt({ldap_ufilter, Host}, Opts, fun check_filter/1, <<"">>), - ConfigGroupFilter = gen_mod:get_opt({ldap_gfilter, Host}, Opts, + ConfigGroupFilter = eldap_utils:get_opt({ldap_gfilter, Host}, Opts, fun check_filter/1, <<"">>), - RosterFilter = gen_mod:get_opt({ldap_rfilter, Host}, Opts, + RosterFilter = eldap_utils:get_opt({ldap_rfilter, Host}, Opts, fun check_filter/1, <<"">>), SubFilter = <<"(&(", UIDAttr/binary, "=", - UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, + UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, UserSubFilter = case ConfigUserFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); - UString -> UString - end, + <<"">> -> + eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); + UString -> UString + end, GroupSubFilter = case ConfigGroupFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, - [{<<"%u">>, <<"*">>}]); - GString -> GString + <<"">> -> + eldap_filter:do_sub(SubFilter, + [{<<"%u">>, <<"*">>}]); + GString -> GString end, Filter = case ConfigFilter of - <<"">> -> SubFilter; - _ -> - <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> - end, - UserFilter = case ConfigFilter of - <<"">> -> UserSubFilter; - _ -> - <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> - end, - GroupFilter = case ConfigFilter of - <<"">> -> GroupSubFilter; - _ -> - <<"(&", GroupSubFilter/binary, ConfigFilter/binary, - ")">> + <<"">> -> SubFilter; + _ -> + <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> end, + UserFilter = case ConfigFilter of + <<"">> -> UserSubFilter; + _ -> + <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> + end, + GroupFilter = case ConfigFilter of + <<"">> -> GroupSubFilter; + _ -> + <<"(&", GroupSubFilter/binary, ConfigFilter/binary, + ")">> + end, +%%%%%%%%%%%%% + GroupBase = gen_mod:get_opt(ldap_group_base, Opts, fun iolist_to_binary/1, + Cfg#eldap_config.base), + GroupIsDN = gen_mod:get_opt(ldap_group_is_dn, Opts, + fun(on) -> true; + (off) -> false; + (false) -> false; + (true) -> true + end, true), + MemberSelMode = gen_mod:get_opt(ldap_member_selection_mode, Opts, + fun(memberattr_normal) -> memberattr_normal; + (memberattr_dn) -> memberattr_dn; + (group_children) -> group_children; + (Invalid) -> + ?ERROR_MSG("Invalid ldap_member_selection_mode '~p'. " + "Value 'memberattr_normal' will be used instead.", + [Invalid]) + end, memberattr_normal), + SubscribeAll = gen_mod:get_opt(ldap_subscribe_all, Opts, + fun(on) -> true; + (off) -> false; + (false) -> false; + (true) -> true + end, false), + % MemberIsDN = (MemberSelMode == member_attr_dn) or (MemberSelMode == group_children), + ShGFilter = gen_mod:get_opt(ldap_shgfilter, Opts, + fun(all) -> all; + (none) -> none; + (S) -> check_filter(S) + end, all), + ShGAttr = gen_mod:get_opt(ldap_shgattr, Opts, + fun iolist_to_binary/1, + << GroupAttr/binary >>), +%%%%%% #state{host = Host, eldap_id = Eldap_ID, - servers = Cfg#eldap_config.servers, - backups = Cfg#eldap_config.backups, + servers = Cfg#eldap_config.servers, + backups = Cfg#eldap_config.backups, port = Cfg#eldap_config.port, - tls_options = Cfg#eldap_config.tls_options, - dn = Cfg#eldap_config.dn, + tls_options = Cfg#eldap_config.tls_options, + dn = Cfg#eldap_config.dn, password = Cfg#eldap_config.password, base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, - uid = UIDAttr, - group_attr = GroupAttr, group_desc = GroupDesc, - user_desc = UserDesc, user_uid = UserUID, - uid_format = UIDAttrFormat, - uid_format_re = UIDAttrFormatRe, filter = Filter, - ufilter = UserFilter, rfilter = RosterFilter, - gfilter = GroupFilter, auth_check = AuthCheck, - user_cache_size = UserCacheSize, - user_cache_validity = UserCacheValidity, - group_cache_size = GroupCacheSize, - group_cache_validity = GroupCacheValidity}. + group_attr = GroupAttr, group_desc = GroupDesc, + user_desc = UserDesc, uid = UserUID, + uid_format = UIDAttrFormat, + uid_format_re = UIDAttrFormatRe, filter = Filter, + ufilter = UserFilter, rfilter = RosterFilter, + gfilter = GroupFilter, auth_check = AuthCheck, + group_base = GroupBase, + member_attr = UIDAttr, + member_selection_mode = MemberSelMode, + group_is_dn = GroupIsDN, + shgfilter = ShGFilter, + shg_attr = ShGAttr, + subscribe_all = SubscribeAll, + roster_cache_size = RosterCacheSize, + roster_cache_validity = RosterCacheValidity}. check_filter(F) -> - NewF = iolist_to_binary(F), - {ok, _} = eldap_filter:parse(NewF), - NewF. + NewF = iolist_to_binary(F), + {ok, _} = eldap_filter:parse(NewF), + NewF. mod_opt_type(deref_aliases) -> fun (never) -> never; @@ -661,6 +936,20 @@ mod_opt_type(ldap_user_cache_validity) -> fun (I) when is_integer(I), I > 0 -> I end; mod_opt_type(ldap_userdesc) -> fun iolist_to_binary/1; mod_opt_type(ldap_useruid) -> fun iolist_to_binary/1; +mod_opt_type(ldap_group_base) -> fun iolist_to_binary/1; +mod_opt_type(ldap_group_is_dn) -> fun(B) when is_boolean(B) -> B end; +mod_opt_type(ldap_member_selection_mode) -> + fun(memberattr_normal) -> memberattr_normal; + (memberattr_dn) -> memberattr_dn; + (group_children) -> group_children + end; +mod_opt_type(ldap_subscribe_all) -> fun(B) when is_boolean(B) -> B end; +mod_opt_type(ldap_shgfilter) -> + fun(all) -> all; + (none) -> none; + (S) -> check_filter(S) + end; +mod_opt_type(ldap_shgattr) -> fun iolist_to_binary/1; mod_opt_type(_) -> [ldap_auth_check, ldap_filter, ldap_gfilter, ldap_group_cache_size, ldap_group_cache_validity, @@ -672,7 +961,9 @@ mod_opt_type(_) -> ldap_deref_aliases, ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, ldap_servers, ldap_tls_cacertfile, ldap_tls_certfile, ldap_tls_depth, - ldap_tls_verify]. + ldap_tls_verify, ldap_group_base, ldap_group_is_dn, + ldap_member_selection_mode, ldap_subscribe_all, + ldap_shgfilter, ldap_shgattr]. opt_type(ldap_filter) -> fun check_filter/1; opt_type(ldap_gfilter) -> fun check_filter/1;