From 31757116fc9af00a16c22793b604aa062a5080e8 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Fri, 5 Nov 2010 02:34:45 +0900 Subject: [PATCH] LDAP shared roster support (thanks to Realloc and Marcin Owsiany) --- src/mod_shared_roster_ldap.erl | 672 +++++++++++++++++++++++++++++++++ 1 file changed, 672 insertions(+) create mode 100644 src/mod_shared_roster_ldap.erl diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl new file mode 100644 index 000000000..b912a5e72 --- /dev/null +++ b/src/mod_shared_roster_ldap.erl @@ -0,0 +1,672 @@ +%%%------------------------------------------------------------------- +%%% 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-2010 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 = 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}, + 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 = 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. + +%%==================================================================== +%% 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}, + {life_time, State#state.user_cache_validity}]), + cache_tab:new(shared_roster_ldap_group, + [{max_size, State#state.group_cache_size}, + {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 iff 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 = jlib: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))}} + 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, + 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, + + 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}], + 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}.