mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
795 lines
30 KiB
Erlang
795 lines
30 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% File : mod_shared_roster_ldap.erl
|
|
%%% Author : Realloc <realloc@realloc.spb.ru>
|
|
%%% Marcin Owsiany <marcin@owsiany.pl>
|
|
%%% Evgeniy Khramtsov <ekhramtsov@process-one.net>
|
|
%%% Description : LDAP shared roster management
|
|
%%% Created : 5 Mar 2005 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2024 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.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%-------------------------------------------------------------------
|
|
-module(mod_shared_roster_ldap).
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-behaviour(gen_mod).
|
|
|
|
%% API
|
|
-export([start/2, stop/1, reload/3]).
|
|
|
|
%% 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_jid_info/4, process_item/2, in_subscription/2,
|
|
out_subscription/1, mod_opt_type/1, mod_options/1,
|
|
depends/2, mod_doc/0]).
|
|
|
|
-include("logger.hrl").
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
|
-include("mod_roster.hrl").
|
|
-include("eldap.hrl").
|
|
-include("translate.hrl").
|
|
|
|
-define(USER_CACHE, shared_roster_ldap_user_cache).
|
|
-define(GROUP_CACHE, shared_roster_ldap_group_cache).
|
|
-define(DISPLAYED_CACHE, shared_roster_ldap_displayed_cache).
|
|
-define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds
|
|
-define(INVALID_SETTING_MSG, "~ts is not properly set! ~ts will not function.").
|
|
|
|
-record(state,
|
|
{host = <<"">> :: binary(),
|
|
eldap_id = <<"">> :: binary(),
|
|
servers = [] :: [binary()],
|
|
backups = [] :: [binary()],
|
|
port = ?LDAP_PORT :: inet:port_number(),
|
|
tls_options = [] :: list(),
|
|
dn = <<"">> :: binary(),
|
|
base = <<"">> :: binary(),
|
|
password = <<"">> :: binary(),
|
|
uid = <<"">> :: binary(),
|
|
deref_aliases = never :: never | searching |
|
|
finding | always,
|
|
group_attr = <<"">> :: binary(),
|
|
group_desc = <<"">> :: binary(),
|
|
user_desc = <<"">> :: binary(),
|
|
user_uid = <<"">> :: binary(),
|
|
uid_format = <<"">> :: binary(),
|
|
uid_format_re :: undefined | re_mp(),
|
|
filter = <<"">> :: binary(),
|
|
ufilter = <<"">> :: binary(),
|
|
rfilter = <<"">> :: binary(),
|
|
gfilter = <<"">> :: binary(),
|
|
user_jid_attr = <<"">> :: binary(),
|
|
auth_check = true :: boolean()}).
|
|
|
|
-record(group_info, {desc, members}).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
start(Host, Opts) ->
|
|
gen_mod:start_child(?MODULE, Host, Opts).
|
|
|
|
stop(Host) ->
|
|
gen_mod:stop_child(?MODULE, Host).
|
|
|
|
reload(Host, NewOpts, _OldOpts) ->
|
|
case init_cache(Host, NewOpts) of
|
|
true ->
|
|
ets_cache:setopts(?USER_CACHE, cache_opts(Host, NewOpts)),
|
|
ets_cache:setopts(?GROUP_CACHE, cache_opts(Host, NewOpts)),
|
|
ets_cache:setopts(?DISPLAYED_CACHE, cache_opts(Host, NewOpts));
|
|
false ->
|
|
ok
|
|
end,
|
|
Proc = gen_mod:get_module_proc(Host, ?MODULE),
|
|
gen_server:cast(Proc, {set_state, parse_options(Host, NewOpts)}).
|
|
|
|
depends(_Host, _Opts) ->
|
|
[{mod_roster, hard}].
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Hooks
|
|
%%--------------------------------------------------------------------
|
|
-spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}].
|
|
get_user_roster(Items, US) ->
|
|
SRUsers = get_user_to_groups_map(US, true),
|
|
{NewItems1, SRUsersRest} = lists:mapfoldl(
|
|
fun(Item = #roster_item{jid = #jid{luser = U1, lserver = S1}}, SRUsers1) ->
|
|
US1 = {U1, S1},
|
|
case dict:find(US1, SRUsers1) of
|
|
{ok, GroupNames} ->
|
|
{Item#roster_item{subscription = both,
|
|
groups = Item#roster_item.groups ++ GroupNames},
|
|
dict:erase(US1, SRUsers1)};
|
|
error ->
|
|
{Item, SRUsers1}
|
|
end
|
|
end,
|
|
SRUsers, Items),
|
|
SRItems = [#roster_item{jid = jid:make(U1, S1),
|
|
name = get_user_name(U1, S1), subscription = both,
|
|
ask = undefined, 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.
|
|
-spec process_item(#roster{}, binary()) -> #roster{}.
|
|
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}
|
|
end.
|
|
|
|
-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid())
|
|
-> {subscription(), ask(), [binary()]}.
|
|
get_jid_info({Subscription, Ask, Groups}, User, Server,
|
|
JID) ->
|
|
LUser = jid:nodeprep(User),
|
|
LServer = jid:nameprep(Server),
|
|
US = {LUser, LServer},
|
|
{U1, S1, _} = 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, none, NewGroups};
|
|
error -> {Subscription, Ask, Groups}
|
|
end.
|
|
|
|
-spec in_subscription(boolean(), presence()) -> boolean().
|
|
in_subscription(Acc, #presence{to = To, from = JID, type = Type}) ->
|
|
#jid{user = User, server = Server} = To,
|
|
process_subscription(in, User, Server, JID, Type, Acc).
|
|
|
|
-spec out_subscription(presence()) -> boolean().
|
|
out_subscription(#presence{from = From, to = JID, type = Type}) ->
|
|
#jid{user = User, server = Server} = From,
|
|
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)),
|
|
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|_]) ->
|
|
process_flag(trap_exit, true),
|
|
Opts = gen_mod:get_module_opts(Host, ?MODULE),
|
|
State = parse_options(Host, Opts),
|
|
init_cache(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_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) ->
|
|
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
|
|
{noreply, State}.
|
|
|
|
handle_cast({set_state, NewState}, _State) ->
|
|
{noreply, NewState};
|
|
handle_cast(Msg, State) ->
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
|
{noreply, State}.
|
|
|
|
handle_info(Info, State) ->
|
|
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
|
{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_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
|
|
%%--------------------------------------------------------------------
|
|
|
|
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.
|
|
[]
|
|
end.
|
|
|
|
get_user_displayed_groups({User, Host}) ->
|
|
{ok, State} = eldap_utils:get_state(Host, ?MODULE),
|
|
ets_cache:lookup(?DISPLAYED_CACHE,
|
|
{User, Host},
|
|
fun () ->
|
|
search_user_displayed_groups(State, User)
|
|
end).
|
|
|
|
search_user_displayed_groups(State, User) ->
|
|
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).
|
|
|
|
get_group_users(Host, Group) ->
|
|
{ok, State} = eldap_utils:get_state(Host, ?MODULE),
|
|
case ets_cache:lookup(?GROUP_CACHE,
|
|
{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 ets_cache:lookup(?GROUP_CACHE,
|
|
{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 ets_cache:lookup(?USER_CACHE,
|
|
{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
|
|
undefined ->
|
|
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:user_exists/2;
|
|
_ -> fun (_U, _S) -> true end
|
|
end,
|
|
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(Entry, Acc) ->
|
|
extract_members(State, Extractor, AuthChecker, Entry, Acc)
|
|
end,
|
|
{Group, []}, LDAPEntries),
|
|
{ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}}
|
|
end.
|
|
|
|
get_member_jid(#state{user_jid_attr = <<>>}, UID, Host) ->
|
|
{jid:nodeprep(UID), Host};
|
|
get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State,
|
|
UID, Host) ->
|
|
Entries = eldap_search(State,
|
|
[eldap_filter:do_sub(<<"(", UIDAttr/binary, "=%u)">>,
|
|
[{<<"%u">>, UID}])],
|
|
[UserJIDAttr]),
|
|
case Entries of
|
|
[#eldap_entry{attributes = [{UserJIDAttr, [MemberJID | _]}]} | _] ->
|
|
try jid:decode(MemberJID) of
|
|
#jid{luser = U, lserver = S} -> {U, S}
|
|
catch
|
|
error:{bad_jid, _} -> {error, Host}
|
|
end;
|
|
_ ->
|
|
{error, error}
|
|
end.
|
|
|
|
extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) ->
|
|
Host = State#state.host,
|
|
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) ->
|
|
{MemberUID, MemberHost} = get_member_jid(State, UID, Host),
|
|
case MemberUID of
|
|
error ->
|
|
L;
|
|
_ ->
|
|
case AuthChecker(MemberUID, MemberHost) of
|
|
true ->
|
|
[{MemberUID, MemberHost} | L];
|
|
_ ->
|
|
L
|
|
end
|
|
end;
|
|
(_, L) -> L
|
|
end, [], lists:map(Extractor, Members)),
|
|
{Desc, [JIDs | JIDsAcc]};
|
|
_ ->
|
|
{DescAcc, JIDsAcc}
|
|
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
|
|
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}
|
|
end.
|
|
|
|
parse_options(Host, Opts) ->
|
|
Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)),
|
|
Cfg = ?eldap_config(mod_shared_roster_ldap_opt, Opts),
|
|
GroupAttr = mod_shared_roster_ldap_opt:ldap_groupattr(Opts),
|
|
GroupDesc = case mod_shared_roster_ldap_opt:ldap_groupdesc(Opts) of
|
|
undefined -> GroupAttr;
|
|
GD -> GD
|
|
end,
|
|
UserDesc = mod_shared_roster_ldap_opt:ldap_userdesc(Opts),
|
|
UserUID = mod_shared_roster_ldap_opt:ldap_useruid(Opts),
|
|
UIDAttr = mod_shared_roster_ldap_opt:ldap_memberattr(Opts),
|
|
UIDAttrFormat = mod_shared_roster_ldap_opt:ldap_memberattr_format(Opts),
|
|
UIDAttrFormatRe = mod_shared_roster_ldap_opt:ldap_memberattr_format_re(Opts),
|
|
JIDAttr = mod_shared_roster_ldap_opt:ldap_userjidattr(Opts),
|
|
AuthCheck = mod_shared_roster_ldap_opt:ldap_auth_check(Opts),
|
|
ConfigFilter = mod_shared_roster_ldap_opt:ldap_filter(Opts),
|
|
ConfigUserFilter = mod_shared_roster_ldap_opt:ldap_ufilter(Opts),
|
|
ConfigGroupFilter = mod_shared_roster_ldap_opt:ldap_gfilter(Opts),
|
|
RosterFilter = mod_shared_roster_ldap_opt:ldap_rfilter(Opts),
|
|
SubFilter = <<"(&(", UIDAttr/binary, "=",
|
|
UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>,
|
|
UserSubFilter = case ConfigUserFilter of
|
|
<<"">> ->
|
|
eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]);
|
|
UString -> UString
|
|
end,
|
|
GroupSubFilter = case ConfigGroupFilter of
|
|
<<"">> ->
|
|
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,
|
|
")">>
|
|
end,
|
|
#state{host = Host, eldap_id = Eldap_ID,
|
|
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,
|
|
password = Cfg#eldap_config.password,
|
|
base = Cfg#eldap_config.base,
|
|
deref_aliases = Cfg#eldap_config.deref_aliases,
|
|
uid = UIDAttr,
|
|
user_jid_attr = JIDAttr,
|
|
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}.
|
|
|
|
init_cache(Host, Opts) ->
|
|
UseCache = use_cache(Host, Opts),
|
|
case UseCache of
|
|
true ->
|
|
CacheOpts = cache_opts(Host, Opts),
|
|
ets_cache:new(?USER_CACHE, CacheOpts),
|
|
ets_cache:new(?GROUP_CACHE, CacheOpts),
|
|
ets_cache:new(?DISPLAYED_CACHE, CacheOpts);
|
|
false ->
|
|
ets_cache:delete(?USER_CACHE),
|
|
ets_cache:delete(?GROUP_CACHE),
|
|
ets_cache:delete(?DISPLAYED_CACHE)
|
|
end,
|
|
UseCache.
|
|
|
|
use_cache(_Host, Opts) ->
|
|
mod_shared_roster_ldap_opt:use_cache(Opts).
|
|
|
|
cache_opts(_Host, Opts) ->
|
|
MaxSize = mod_shared_roster_ldap_opt:cache_size(Opts),
|
|
CacheMissed = mod_shared_roster_ldap_opt:cache_missed(Opts),
|
|
LifeTime = mod_shared_roster_ldap_opt:cache_life_time(Opts),
|
|
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
|
|
|
|
mod_opt_type(ldap_auth_check) ->
|
|
econf:bool();
|
|
mod_opt_type(ldap_gfilter) ->
|
|
econf:ldap_filter();
|
|
mod_opt_type(ldap_groupattr) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_groupdesc) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_memberattr) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_memberattr_format) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_memberattr_format_re) ->
|
|
econf:re();
|
|
mod_opt_type(ldap_rfilter) ->
|
|
econf:ldap_filter();
|
|
mod_opt_type(ldap_ufilter) ->
|
|
econf:ldap_filter();
|
|
mod_opt_type(ldap_userdesc) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_useruid) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_userjidattr) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_backups) ->
|
|
econf:list(econf:domain(), [unique]);
|
|
mod_opt_type(ldap_base) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_deref_aliases) ->
|
|
econf:enum([never, searching, finding, always]);
|
|
mod_opt_type(ldap_encrypt) ->
|
|
econf:enum([tls, starttls, none]);
|
|
mod_opt_type(ldap_filter) ->
|
|
econf:ldap_filter();
|
|
mod_opt_type(ldap_password) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_port) ->
|
|
econf:port();
|
|
mod_opt_type(ldap_rootdn) ->
|
|
econf:binary();
|
|
mod_opt_type(ldap_servers) ->
|
|
econf:list(econf:domain(), [unique]);
|
|
mod_opt_type(ldap_tls_cacertfile) ->
|
|
econf:pem();
|
|
mod_opt_type(ldap_tls_certfile) ->
|
|
econf:pem();
|
|
mod_opt_type(ldap_tls_depth) ->
|
|
econf:non_neg_int();
|
|
mod_opt_type(ldap_tls_verify) ->
|
|
econf:enum([hard, soft, false]);
|
|
mod_opt_type(ldap_uids) ->
|
|
econf:either(
|
|
econf:list(
|
|
econf:and_then(
|
|
econf:binary(),
|
|
fun(U) -> {U, <<"%u">>} end)),
|
|
econf:map(econf:binary(), econf:binary(), [unique]));
|
|
mod_opt_type(use_cache) ->
|
|
econf:bool();
|
|
mod_opt_type(cache_size) ->
|
|
econf:pos_int(infinity);
|
|
mod_opt_type(cache_missed) ->
|
|
econf:bool();
|
|
mod_opt_type(cache_life_time) ->
|
|
econf:timeout(second, infinity).
|
|
|
|
-spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} |
|
|
{atom(), any()}].
|
|
mod_options(Host) ->
|
|
[{ldap_auth_check, true},
|
|
{ldap_gfilter, <<"">>},
|
|
{ldap_groupattr, <<"cn">>},
|
|
{ldap_groupdesc, undefined},
|
|
{ldap_memberattr, <<"memberUid">>},
|
|
{ldap_memberattr_format, <<"%u">>},
|
|
{ldap_memberattr_format_re, undefined},
|
|
{ldap_rfilter, <<"">>},
|
|
{ldap_ufilter, <<"">>},
|
|
{ldap_userdesc, <<"cn">>},
|
|
{ldap_useruid, <<"cn">>},
|
|
{ldap_userjidattr, <<"">>},
|
|
{ldap_backups, ejabberd_option:ldap_backups(Host)},
|
|
{ldap_base, ejabberd_option:ldap_base(Host)},
|
|
{ldap_uids, ejabberd_option:ldap_uids(Host)},
|
|
{ldap_deref_aliases, ejabberd_option:ldap_deref_aliases(Host)},
|
|
{ldap_encrypt, ejabberd_option:ldap_encrypt(Host)},
|
|
{ldap_password, ejabberd_option:ldap_password(Host)},
|
|
{ldap_port, ejabberd_option:ldap_port(Host)},
|
|
{ldap_rootdn, ejabberd_option:ldap_rootdn(Host)},
|
|
{ldap_servers, ejabberd_option:ldap_servers(Host)},
|
|
{ldap_filter, ejabberd_option:ldap_filter(Host)},
|
|
{ldap_tls_certfile, ejabberd_option:ldap_tls_certfile(Host)},
|
|
{ldap_tls_cacertfile, ejabberd_option:ldap_tls_cacertfile(Host)},
|
|
{ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)},
|
|
{ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)},
|
|
{use_cache, ejabberd_option:use_cache(Host)},
|
|
{cache_size, ejabberd_option:cache_size(Host)},
|
|
{cache_missed, ejabberd_option:cache_missed(Host)},
|
|
{cache_life_time, ejabberd_option:cache_life_time(Host)}].
|
|
|
|
mod_doc() ->
|
|
#{desc =>
|
|
[?T("This module lets the server administrator automatically "
|
|
"populate users' rosters (contact lists) with entries based on "
|
|
"users and groups defined in an LDAP-based directory."), "",
|
|
?T("NOTE: 'mod_shared_roster_ldap' depends on 'mod_roster' being "
|
|
"enabled. Roster queries will return '503' errors if "
|
|
"'mod_roster' is not enabled."), "",
|
|
?T("The module accepts many configuration options. Some of them, "
|
|
"if unspecified, default to the values specified for the top "
|
|
"level of configuration. This lets you avoid specifying, for "
|
|
"example, the bind password in multiple places."), "",
|
|
?T("- Filters: 'ldap_rfilter', 'ldap_ufilter', 'ldap_gfilter', "
|
|
"'ldap_filter'. These options specify LDAP filters used to "
|
|
"query for shared roster information. All of them are run "
|
|
"against the ldap_base."),
|
|
?T("- Attributes: 'ldap_groupattr', 'ldap_groupdesc', "
|
|
"'ldap_memberattr', 'ldap_userdesc', 'ldap_useruid'. These "
|
|
"options specify the names of the attributes which hold "
|
|
"interesting data in the entries returned by running filters "
|
|
"specified with the filter options."),
|
|
?T("- Control parameters: 'ldap_auth_check', "
|
|
"'ldap_group_cache_validity', 'ldap_memberattr_format', "
|
|
"'ldap_memberattr_format_re', 'ldap_user_cache_validity'. "
|
|
"These parameters control the behaviour of the module."),
|
|
?T("- Connection parameters: The module also accepts the "
|
|
"connection parameters, all of which default to the top-level "
|
|
"parameter of the same name, if unspecified. "
|
|
"See http://../ldap/#ldap-connection[LDAP Connection] "
|
|
"section for more information about them."), "",
|
|
?T("Check also the http://../ldap/#ldap-examples"
|
|
"[Configuration examples] section to get details about "
|
|
"retrieving the roster, "
|
|
"and configuration examples including Flat DIT and Deep DIT.")],
|
|
opts =>
|
|
[
|
|
%% Filters:
|
|
{ldap_rfilter,
|
|
#{desc =>
|
|
?T("So called \"Roster Filter\". Used to find names of "
|
|
"all \"shared roster\" groups. See also the "
|
|
"'ldap_groupattr' parameter. If unspecified, defaults to "
|
|
"the top-level parameter of the same name. You must "
|
|
"specify it in some place in the configuration, there is "
|
|
"no default.")}},
|
|
{ldap_gfilter,
|
|
#{desc =>
|
|
?T("\"Group Filter\", used when retrieving human-readable "
|
|
"name (a.k.a. \"Display Name\") and the members of a "
|
|
"group. See also the parameters 'ldap_groupattr', "
|
|
"'ldap_groupdesc' and 'ldap_memberattr'. If unspecified, "
|
|
"defaults to the top-level parameter of the same name. "
|
|
"If that one also is unspecified, then the filter is "
|
|
"constructed exactly like \"User Filter\".")}},
|
|
{ldap_ufilter,
|
|
#{desc =>
|
|
?T("\"User Filter\", used for retrieving the human-readable "
|
|
"name of roster entries (usually full names of people in "
|
|
"the roster). See also the parameters 'ldap_userdesc' and "
|
|
"'ldap_useruid'. For more information check the LDAP "
|
|
"http://../ldap/#filters[Filters] section.")}},
|
|
{ldap_filter,
|
|
#{desc =>
|
|
?T("Additional filter which is AND-ed together "
|
|
"with \"User Filter\" and \"Group Filter\". "
|
|
"For more information check the LDAP "
|
|
"http://../ldap/#filters[Filters] section.")}},
|
|
%% Attributes:
|
|
{ldap_groupattr,
|
|
#{desc =>
|
|
?T("The name of the attribute that holds the group name, and "
|
|
"that is used to differentiate between them. Retrieved "
|
|
"from results of the \"Roster Filter\" "
|
|
"and \"Group Filter\". Defaults to 'cn'.")}},
|
|
|
|
{ldap_groupdesc,
|
|
#{desc =>
|
|
?T("The name of the attribute which holds the human-readable "
|
|
"group name in the objects you use to represent groups. "
|
|
"Retrieved from results of the \"Group Filter\". "
|
|
"Defaults to whatever 'ldap_groupattr' is set.")}},
|
|
|
|
{ldap_memberattr,
|
|
#{desc =>
|
|
?T("The name of the attribute which holds the IDs of the "
|
|
"members of a group. Retrieved from results of the "
|
|
"\"Group Filter\". Defaults to 'memberUid'. The name of "
|
|
"the attribute differs depending on the objectClass you "
|
|
"use for your group objects, for example: "
|
|
"'posixGroup' -> 'memberUid'; 'groupOfNames' -> 'member'; "
|
|
"'groupOfUniqueNames' -> 'uniqueMember'.")}},
|
|
{ldap_userdesc,
|
|
#{desc =>
|
|
?T("The name of the attribute which holds the human-readable "
|
|
"user name. Retrieved from results of the "
|
|
"\"User Filter\". Defaults to 'cn'.")}},
|
|
{ldap_useruid,
|
|
#{desc =>
|
|
?T("The name of the attribute which holds the ID of a roster "
|
|
"item. Value of this attribute in the roster item objects "
|
|
"needs to match the ID retrieved from the "
|
|
"'ldap_memberattr' attribute of a group object. "
|
|
"Retrieved from results of the \"User Filter\". "
|
|
"Defaults to 'cn'.")}},
|
|
{ldap_userjidattr,
|
|
#{desc =>
|
|
?T("The name of the attribute which is used to map user id "
|
|
"to XMPP jid. If not specified (and that is default value "
|
|
"of this option), user jid will be created from user id and "
|
|
" this module host.")}},
|
|
%% Control parameters:
|
|
{ldap_memberattr_format,
|
|
#{desc =>
|
|
?T("A globbing format for extracting user ID from the value "
|
|
"of the attribute named by 'ldap_memberattr'. Defaults "
|
|
"to '%u', which means that the whole value is the member "
|
|
"ID. If you change it to something different, you may "
|
|
"also need to specify the User and Group Filters "
|
|
"manually; see section Filters.")}},
|
|
|
|
{ldap_memberattr_format_re,
|
|
#{desc =>
|
|
?T("A regex for extracting user ID from the value of the "
|
|
"attribute named by 'ldap_memberattr'. Check the LDAP "
|
|
"http://../ldap/#control-parameters"
|
|
"[Control Parameters] section.")}},
|
|
{ldap_auth_check,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("Whether the module should check (via the ejabberd "
|
|
"authentication subsystem) for existence of each user in "
|
|
"the shared LDAP roster. Set to 'false' if you want to "
|
|
"disable the check. Default value is 'true'.")}}] ++
|
|
[{Opt,
|
|
#{desc =>
|
|
{?T("Same as top-level _`~s`_ option, but "
|
|
"applied to this module only."), [Opt]}}}
|
|
|| Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases,
|
|
ldap_encrypt, ldap_password, ldap_port, ldap_rootdn,
|
|
ldap_servers, ldap_tls_certfile, ldap_tls_cacertfile,
|
|
ldap_tls_depth, ldap_tls_verify, use_cache, cache_size,
|
|
cache_missed, cache_life_time]]}.
|