%%%------------------------------------------------------------------- %%% File : mod_shared_roster_ldap.erl %%% Author : Realloc %%% Marcin Owsiany %%% Evgeniy Khramtsov %%% Description : LDAP shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2011 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(mod_shared_roster_ldap). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start_link/2, start/2, stop/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([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("mod_roster.hrl"). -include("eldap/eldap.hrl"). -define(CACHE_SIZE, 1000). -define(USER_CACHE_VALIDITY, 300). %% in seconds -define(GROUP_CACHE_VALIDITY, 300). %% in seconds -define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds -record(state, {host, eldap_id, servers, backups, port, tls_options, dn, base, password, uid, group_attr, group_desc, user_desc, user_uid, uid_format, uid_format_re, filter, ufilter, rfilter, gfilter, auth_check, user_cache_size, group_cache_size, user_cache_validity, group_cache_validity}). -record(group_info, {desc, members}). %%==================================================================== %% API %%==================================================================== start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). 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), supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- get_user_roster(Items, {U, S} = US) -> SRUsers = get_user_to_groups_map(US, true), %% 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}, 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 -> %% Roster item cannot be removed: %% We simply reset the original groups: RosterItem#roster{subscription = both, ask = none, groups=GroupNames}; _ -> RosterItem#roster{subscription = both, ask = none} end. get_subscription_lists({F, T}, User, Server) -> LUser = exmpp_stringprep:nodeprep(User), LServer = exmpp_stringprep: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 = exmpp_stringprep:nodeprep(User), LServer = exmpp_stringprep:nameprep(Server), US = {LUser, LServer}, %% TODO BADLOP: I don't know what version is correct, A or B: %% %% A) as binaries {U1, S1, _} = jlib:short_prepd_jid(JID), US1 = {U1, S1}, %% %% B) as strings %% US1 = {exmpp_jid:prep_node_as_list(JID), exmpp_jid:prep_domain_as_list(JID)}, 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} 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 = exmpp_stringprep:nodeprep(User), LServer = exmpp_stringprep:nameprep(Server), US = {LUser, LServer}, %% TODO BADLOP: I don't know what version is correct, A or B: %% %% A) as binaries {U1, S1, _} = jlib:short_prepd_jid(JID), US1 = {U1, S1}, %% %% B) as strings %% US1 = {exmpp_jid:prep_node_as_list(JID), exmpp_jid:prep_domain_as_list(JID)}, 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. %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host, Opts]) -> State = parse_options(Host, Opts), cache_tab:new(shared_roster_ldap_user, [{max_size, State#state.user_cache_size}, {lru, false}, % We don't need LRU algorithm {life_time, State#state.user_cache_validity}]), cache_tab:new(shared_roster_ldap_group, [{max_size, State#state.group_cache_size}, {lru, false}, % We don't need LRU algorithm {life_time, State#state.group_cache_validity}]), 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_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), {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_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_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). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% For a given user, map all his shared roster contacts to groups they are %% members of. Skip the user himself if SkipUS is true. 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). %% Pass given FilterParseArgs to eldap_filter:parse, and if successful, run and %% return the resulting filter, retrieving given AttributesList. Return the %% result entries. On any error silently return an empty list of results. %% %% Eldap server ID and base DN for the query are both retrieved from the State %% record. eldap_search(State, FilterParseArgs, AttributesList) -> case apply(eldap_filter, parse, FilterParseArgs) of {ok, EldapFilter} -> %% Filter parsing succeeded case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, EldapFilter}, {timeout, ?LDAP_SEARCH_TIMEOUT}, {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. [] end. get_user_displayed_groups({_User, Host}) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), GroupAttr = State#state.group_attr, Entries = eldap_search(State, [State#state.rfilter], [GroupAttr]), Reply = lists:flatmap( fun(#eldap_entry{attributes = Attrs}) -> case Attrs of [{GroupAttr, ValuesList}] -> ValuesList; _ -> [] end end, Entries), lists:usort(Reply). 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; _ -> [] 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 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 end. 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 = exmpp_stringprep:nodeprep(UID), case AuthChecker(PUID, Host) of true -> [{PUID, Host} | L]; _ -> L 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))}} 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 /= "" -> %% By returning "" get_ldap_attr means "not found" {ok, Desc}; _ -> error end; [] -> 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 = string:sub_string(String, First+1, First+Len), {ok,Result}; _ -> {error, badmatch} end. 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, LDAPBackups = case gen_mod:get_opt(ldap_backups, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_servers, Host}); Backups -> Backups end, LDAPEncrypt = case gen_mod:get_opt(ldap_encrypt, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_encrypt, Host}); E -> E end, LDAPTLSVerify = case gen_mod:get_opt(ldap_tls_verify, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_tls_verify, Host}); Verify -> Verify end, LDAPTLSCAFile = case gen_mod:get_opt(ldap_tls_cacertfile, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_tls_cacertfile, Host}); CAFile -> CAFile end, LDAPTLSDepth = case gen_mod:get_opt(ldap_tls_depth, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_tls_depth, Host}); Depth -> Depth end, LDAPPort = case gen_mod:get_opt(ldap_port, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_port, Host}) of undefined -> case LDAPEncrypt of tls -> ?LDAPS_PORT; starttls -> ?LDAP_PORT; _ -> ?LDAP_PORT end; 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 -> GroupAttr; GD -> GD end, UserDesc = case gen_mod:get_opt(ldap_userdesc, Opts, undefined) of undefined -> "cn"; UD -> UD end, UserUID = case gen_mod:get_opt(ldap_useruid, Opts, undefined) of undefined -> "cn"; UU -> UU 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, UIDAttrFormatRe = case gen_mod:get_opt(ldap_memberattr_format_re, Opts, undefined) of undefined -> ""; UAFre -> case catch re:compile(UAFre) of {ok, MP} -> MP; _ -> ?ERROR_MSG("Invalid ldap_memberattr_format_re '~s' " "or no RE support in this erlang version. " "ldap_memberattr_format '~s' will be used " "instead.", [UAFre, UIDAttrFormat]), "" end end, AuthCheck = case gen_mod:get_opt(ldap_auth_check, Opts, undefined) of undefined -> true; on -> true; AC -> AC 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, UserCacheValidity = case gen_mod:get_opt(ldap_user_cache_validity, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_user_cache_validity, Host}) of undefined -> ?USER_CACHE_VALIDITY; UVSeconds -> UVSeconds end; UVSeconds -> UVSeconds end, GroupCacheValidity = case gen_mod:get_opt(ldap_group_cache_validity, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_group_cache_validity, Host}) of undefined -> ?GROUP_CACHE_VALIDITY; GVSeconds -> GVSeconds end; GVSeconds -> GVSeconds end, UserCacheSize = case gen_mod:get_opt(ldap_user_cache_size, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_user_cache_size, Host}) of undefined -> ?CACHE_SIZE; USSeconds -> USSeconds end; USSeconds -> USSeconds end, GroupCacheSize = case gen_mod:get_opt(ldap_group_cache_size, Opts, undefined) of undefined -> case ejabberd_config:get_local_option({ldap_group_cache_size, Host}) of undefined -> ?CACHE_SIZE; GSSeconds -> GSSeconds end; GSSeconds -> GSSeconds end, ConfigFilter = case gen_mod:get_opt(ldap_filter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_filter, Host}); F -> F end, ConfigUserFilter = case gen_mod:get_opt(ldap_ufilter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_ufilter, Host}); UF -> UF end, ConfigGroupFilter = case gen_mod:get_opt(ldap_gfilter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_gfilter, Host}); GF -> GF end, RosterFilter = case gen_mod:get_opt(ldap_rfilter, Opts, undefined) of undefined -> ejabberd_config:get_local_option({ldap_rfilter, Host}); RF -> RF end, lists:foreach(fun eldap_utils:check_filter/1, [ConfigFilter, ConfigUserFilter, ConfigGroupFilter, RosterFilter]), SubFilter = "(&("++UIDAttr++"="++UIDAttrFormat++")("++GroupAttr++"=%g))", UserSubFilter = case ConfigUserFilter of undefined -> eldap_filter:do_sub(SubFilter, [{"%g", "*"}]); "" -> eldap_filter:do_sub(SubFilter, [{"%g", "*"}]); UString -> UString end, GroupSubFilter = case ConfigGroupFilter of undefined -> eldap_filter:do_sub(SubFilter, [{"%u", "*"}]); "" -> eldap_filter:do_sub(SubFilter, [{"%u", "*"}]); GString -> GString end, 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, backups = LDAPBackups, port = LDAPPort, tls_options = [{encrypt, LDAPEncrypt}, {tls_verify, LDAPTLSVerify}, {tls_cacertfile, LDAPTLSCAFile}, {tls_depth, LDAPTLSDepth}], dn = RootDN, base = LDAPBase, password = Password, 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}.