%%%---------------------------------------------------------------------- %%% File : mod_shared_roster_ldap.erl %%% Author : Alexey Shchepin %%% Purpose : LDAP shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin %%% Id : $Id: mod_shared_roster.erl 24 2005-04-14 01:15:31Z alexey $ %%%---------------------------------------------------------------------- %%%---------------------------------------------------------------------- %%% Some changes to make it AD friendly and more usable :-) %%% realloc@realloc.spb.ru %%%---------------------------------------------------------------------- -module(mod_shared_roster_ldap). -author('alexey@sevcom.net'). -behaviour(gen_server). -behaviour(gen_mod). %% gen_server callbacks -export([ init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3 ]). -export([ start/2, start_link/2, stop/1, get_user_roster/2, get_subscription_lists/3, get_jid_info/4, process_item/2, in_subscription/6, out_subscription/4 ]). -include("ejabberd.hrl"). -include("eldap/eldap.hrl"). -include("jlib.hrl"). -include("mod_roster.hrl"). -record(state, { host, eldap_id, servers, port, dn, base, password, uid, group_attr, group_desc, user_desc, uid_format, filter, ufilter, rfilter, gfilter }). -define(LDAP_REQUEST_TIMEOUT, 10000). %% Unused callbacks. handle_cast(_Request, State) -> {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. handle_info(_Info, State) -> {noreply, State}. %% ----- start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = { Proc, {?MODULE, start_link, [Host, Opts]}, permanent, 1000, worker, [?MODULE] }, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:call(Proc, stop), supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). terminate(_Reason, State) -> Host = State#state.host, ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:delete(roster_in_subscription, Host, ?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), ejabberd_hooks:delete(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, ?MODULE, process_item, 50). init([Host, Opts]) -> State = parse_options(Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, ?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), eldap:start_link(State#state.eldap_id, State#state.servers, State#state.port, State#state.dn, State#state.password), {ok, State}. get_user_roster(Items, US) -> {U, S} = US, DisplayedGroups = get_user_displayed_groups(US), %% Get shared roster users in all groups and remove self: SRUsers = lists:foldl( fun(Group, Acc1) -> lists:foldl( fun(User, Acc2) -> if User == US -> Acc2; true -> dict:append(User, get_group_name(S, Group), Acc2) end end, Acc1, get_group_users(S, Group)) end, dict:new(), DisplayedGroups), %% 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 dict:find(US1, SRUsers1) of {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)], 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}, DisplayedGroups = get_user_displayed_groups(USFrom), CommonGroups = lists:filter(fun(Group) -> is_user_in_group(USTo, Group, Server) end, DisplayedGroups), case CommonGroups of [] -> RosterItem; %% Roster item cannot be removed: We simply reset the original groups: _ when RosterItem#roster.subscription == remove -> GroupNames = lists:map(fun(Group) -> get_group_name(Host, Group) end, CommonGroups), RosterItem#roster{subscription = both, ask = none, groups=[GroupNames]}; _ -> RosterItem#roster{subscription = both, ask = none} end. get_subscription_lists({F, T}, User, Server) -> LUser = jlib:nodeprep(User), LServer = jlib: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], {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}. get_jid_info({Subscription, Groups}, User, Server, JID) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jlib:jid_tolower(JID), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:foldl( fun(Group, Acc1) -> lists:foldl( fun(User1, Acc2) -> dict:append( User1, get_group_name(LServer, Group), Acc2) end, Acc1, get_group_users(LServer, Group)) end, dict:new(), DisplayedGroups), case dict:find(US1, SRUsers) of {ok, GroupNames} -> NewGroups = if Groups == [] -> GroupNames; true -> Groups end, {both, NewGroups}; error -> {Subscription, Groups} end. 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(Direction, User, Server, JID, _Type, Acc) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jlib:jid_tolower(jlib:jid_remove_resource(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 end. get_group_users(Host, Group) -> make_request(Host, {get_group_users, Group}, []). get_group_name(Host, Group) -> make_request(Host, {get_group_name, Group}, Group). get_user_displayed_groups({User, Host}) -> make_request(Host, {get_user_displayed_groups, User}, []). is_user_in_group({User, _Server}, Group, Host) -> make_request(Host, {is_user_in_group, User, Group}, false). get_user_name(User, Host) -> make_request(Host, {get_user_name, User},[]). %%%----------------------- %%% Internal functions. %%%----------------------- handle_call({get_user_displayed_groups, User}, _From, State) -> GroupAttr = State#state.group_attr, Reply = case eldap_filter:parse(State#state.rfilter) of {ok, EldapFilter} -> case eldap:search(State#state.eldap_id, [ {base, State#state.base}, {filter, EldapFilter}, {attributes, [GroupAttr]}]) of #eldap_search_result{entries = Es} -> lists:usort(lists:flatmap( fun(#eldap_entry{attributes = Attrs}) -> case Attrs of [{GroupAttr, ValuesList}] -> ValuesList; _ -> [] end end, Es)); _ -> [] end; _ -> [] end, {reply, Reply, State}; handle_call({get_group_name, Group}, _From, State) -> GroupDescAttr = State#state.group_desc, Reply = case eldap_filter:parse(State#state.gfilter, [{"%g", Group}]) of {ok, EldapFilter} -> case eldap:search(State#state.eldap_id, [ {base, State#state.base}, {filter, EldapFilter}, {attributes, [GroupDescAttr]}]) of #eldap_search_result{entries = [ #eldap_entry{attributes = [{GroupDescAttr, GroupName} | _]} ]} -> GroupName; _ -> Group end; _ -> Group end, {reply, Reply, State}; handle_call({get_user_name, User}, _From, State) -> UserDescAttr = State#state.user_desc, Reply = case eldap_filter:parse(State#state.ufilter, [{"%u", User}]) of {ok, EldapFilter} -> case eldap:search(State#state.eldap_id, [ {base, State#state.base}, {filter, EldapFilter}, {attributes, [UserDescAttr]}]) of #eldap_search_result{entries = [ #eldap_entry{attributes = [{UserDescAttr, UserName} | _]} ]} -> UserName; _ -> User end; _ -> User end, {reply, Reply, State}; handle_call({get_group_users, Group}, _From, State) -> UIDAttr = State#state.uid, UAF = State#state.uid_format, Host = State#state.host, Reply = case eldap_filter:parse(State#state.gfilter, [{"%g", Group}]) of {ok, EldapFilter} -> case eldap:search(State#state.eldap_id, [ {base, State#state.base}, {filter, EldapFilter}, {attributes, [UIDAttr]}]) of #eldap_search_result{entries = Es} -> lists:usort(lists:flatmap( fun(#eldap_entry{attributes = Attrs}) -> case Attrs of [{UIDAttr, UsersList}] -> lists:foldl(fun(User, Acc) -> case catch get_user_part(User, UAF) of {ok, U} -> case ejabberd_auth:is_user_exists(U, Host) of true -> [{U, Host} | Acc]; _ -> Acc end; _ -> Acc end end, [], UsersList); _ -> [] end end, Es)); _ -> [] end; _ -> [] end, {reply, Reply, State}; handle_call({is_user_in_group, User, Group}, _From, State) -> Reply = case eldap_filter:parse(State#state.filter, [{"%u", User}, {"%g", Group}]) of {ok, EldapFilter} -> case eldap:search(State#state.eldap_id, [ {base, State#state.base}, {filter, EldapFilter}, {attributes, ["dn"]}]) of #eldap_search_result{entries = [_|_]} -> true; _ -> false end; _ -> false end, {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> {reply, bad_request, State}. %%%----------------------- %%% Auxiliary functions. %%%----------------------- parse_options(Host, Opts) -> Eldap_ID = atom_to_list(gen_mod:get_module_proc(Host, ?MODULE)), LDAPServers = case gen_mod:get_opt(ldap_servers, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_servers, Host}); S -> S end, LDAPPort = case gen_mod:get_opt(ldap_port, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_port, Host}) of undefined -> 389; P -> P end; P -> P end, LDAPBase = case gen_mod:get_opt(ldap_base, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_base, Host}); B -> B end, GroupAttr = case gen_mod:get_opt(ldap_groupattr, Opts, undefined) of undefined -> "cn"; GA -> GA end, GroupDesc = case gen_mod:get_opt(ldap_groupdesc, Opts, undefined) of undefined -> "cn"; GD -> GD end, UserDesc = case gen_mod:get_opt(ldap_userdesc, Opts, undefined) of undefined -> "cn"; UD -> UD end, UIDAttr = case gen_mod:get_opt(ldap_memberattr, Opts, undefined) of undefined -> "memberUid"; UA -> UA end, UIDAttrFormat = case gen_mod:get_opt(ldap_memberattr_format, Opts, undefined) of undefined -> "%u"; UAF -> UAF end, RootDN = case gen_mod:get_opt(ldap_rootdn, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_rootdn, Host}) of undefined -> ""; RDN -> RDN end; RDN -> RDN end, Password = case gen_mod:get_opt(ldap_password, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_password, Host}) of undefined -> ""; Pass -> Pass end; Pass -> Pass end, ConfigFilter = case gen_mod:get_opt(ldap_filter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_filter, Host}); F -> F end, RosterFilter = case gen_mod:get_opt(ldap_rfilter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_rfilter, Host}); RF -> RF end, SubFilter = "(&("++UIDAttr++"="++UIDAttrFormat++")("++GroupAttr++"=%g))", UserSubFilter = eldap_filter:do_sub(SubFilter, [{"%g", "*"}]), GroupSubFilter = eldap_filter:do_sub(SubFilter, [{"%u", "*"}]), Filter = case ConfigFilter of undefined -> SubFilter; "" -> SubFilter; _ -> "(&" ++ SubFilter ++ ConfigFilter ++ ")" end, UserFilter = case ConfigFilter of undefined -> UserSubFilter; "" -> UserSubFilter; _ -> "(&" ++ UserSubFilter ++ ConfigFilter ++ ")" end, GroupFilter = case ConfigFilter of undefined -> GroupSubFilter; "" -> GroupSubFilter; _ -> "(&" ++ GroupSubFilter ++ ConfigFilter ++ ")" end, #state{ host = Host, eldap_id = Eldap_ID, servers = LDAPServers, port = LDAPPort, dn = RootDN, base = LDAPBase, password = Password, uid = UIDAttr, group_attr = GroupAttr, group_desc = GroupDesc, user_desc = UserDesc, uid_format = UIDAttrFormat, filter = Filter, ufilter = UserFilter, rfilter = RosterFilter, gfilter = GroupFilter }. get_user_part(String, Pattern) -> F = fun(S, P) -> First = string:str(P, "%u"), TailLength = length(P) - (First+1), string:sub_string(S, First, length(S) - TailLength) end, case catch F(String, Pattern) of {'EXIT', _} -> {error, badmatch}; Result -> case regexp:sub(Pattern, "%u", Result) of {ok, String, _} -> {ok, Result}; _ -> {error, badmatch} end end. make_request(Host, Request, Fallback) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), case catch gen_server:call(Proc, Request, ?LDAP_REQUEST_TIMEOUT) of {'EXIT', _} -> Fallback; Result -> Result end.