%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_ldap.erl %%% Author : Alexey Shchepin %%% Purpose : Authentification via LDAP %%% Created : 12 Dec 2004 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(ejabberd_auth_ldap). -author('alexey@process-one.net'). -behaviour(gen_server). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3 ]). %% External exports -export([start/1, stop/1, start_link/1, set_password/3, check_password/3, check_password/5, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, get_vh_registered_users_number/1, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, remove_user/3, plain_password_required/0 ]). -include("ejabberd.hrl"). -include("eldap/eldap.hrl"). -record(state, {host, eldap_id, bind_eldap_id, servers, backups, port, tls_options, dn, password, base, uids, ufilter, lfilter, %% Local filter (performed by ejabberd, not LDAP) dn_filter, dn_filter_attrs }). %% Unused callbacks. handle_cast(_Request, State) -> {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. handle_info(_Info, State) -> {noreply, State}. %% ----- -define(LDAP_SEARCH_TIMEOUT, 5). % Timeout for LDAP search queries in seconds %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %% @spec (Host) -> term() %% Host = string() start(Host) -> ?DEBUG("Starting ~p for ~p.", [?MODULE, Host]), case ejabberd_config:get_host_option(Host, ldap_servers) of undefined -> check_bad_config(Host); {host, _Host} -> ok; _ -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = { Proc, {?MODULE, start_link, [Host]}, transient, 1000, worker, [?MODULE] }, supervisor:start_child(ejabberd_sup, ChildSpec) end. check_bad_config(Host) -> case ejabberd_config:get_local_option({ldap_servers, Host}) of undefined -> ?ERROR_MSG("Can't start ~p for host ~p: missing ldap_servers configuration", [?MODULE, Host]), {error, bad_config}; _ -> ok end. %% @spec (Host) -> term() %% Host = string() stop(Host) -> case ejabberd_config:get_host_option(Host, ldap_servers) of undefined -> ok; {host, _Host} -> ok; _ -> 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) end. %% @spec (Host) -> term() %% Host = string() start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, Host, []). %% @hidden terminate(_Reason, _State) -> ok. %% @spec (Host) -> {ok, State} %% Host = string() %% State = term() init(Host) -> State = parse_options(Host), 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), eldap_pool:start_link(State#state.bind_eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), {ok, State}. %% @spec () -> true plain_password_required() -> true. %% @spec (User, Server, Password) -> bool() %% User = string() %% Server = string() %% Password = string() check_password(User, Server, Password) -> %% In LDAP spec: empty password means anonymous authentication. %% As ejabberd is providing other anonymous authentication mechanisms %% we simply prevent the use of LDAP anonymous authentication. if Password == "" -> false; true -> case catch check_password_ldap(User, Server, Password) of {'EXIT', _} -> false; Result -> Result end end. %% @spec (User, Server, Password, Digest, DigestGen) -> bool() %% User = string() %% Server = string() %% Password = string() %% Digest = string() %% DigestGen = function() check_password(User, Server, Password, _Digest, _DigestGen) -> check_password(User, Server, Password). %% @spec (User, Server, Password) -> {error, Reason} | ok %% User = string() %% Server = string() %% Password = string() %% Reason = term() set_password(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, Server, State) of false -> {error, user_not_found}; DN -> eldap_pool:modify_passwd(State#state.eldap_id, DN, Password) end. %% @spec (User, Server, Password) -> {error, not_allowed} %% User = string() %% Server = string() %% Password = string() try_register(_User, _Server, _Password) -> {error, not_allowed}. %% @spec () -> [{LUser, LServer}] %% LUser = string() %% LServer = string() dirty_get_registered_users() -> Servers = ejabberd_config:get_vh_by_auth_method(ldap), lists:flatmap( fun(Server) -> get_vh_registered_users(Server) end, Servers). %% @spec (Server) -> [{LUser, LServer}] %% Server = string() %% LUser = string() %% LServer = string() get_vh_registered_users(Server) -> case catch get_vh_registered_users_ldap(Server) of {'EXIT', _} -> []; Result -> Result end. %% @spec (Server) -> Users_Number %% Server = string() %% Users_Number = integer() get_vh_registered_users_number(Server) -> length(get_vh_registered_users(Server)). %% @spec (User, Server) -> bool() %% User = string() %% Server = string() get_password(_User, _Server) -> false. %% @spec (User, Server) -> nil() %% User = string() %% Server = string() get_password_s(_User, _Server) -> "". %% @spec (User, Server) -> true | false | {error, Error} %% User = string() %% Server = string() is_user_exists(User, Server) -> case catch is_user_exists_ldap(User, Server) of {'EXIT', Error} -> {error, Error}; Result -> Result end. %% @spec (User, Server) -> {error, not_allowed} %% User = string() %% Server = string() remove_user(_User, _Server) -> {error, not_allowed}. %% @spec (User, Server, Password) -> not_allowed %% User = string() %% Server = string() %% Password = string() remove_user(_User, _Server, _Password) -> not_allowed. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %% @spec (User, Server, Password) -> bool() %% User = string() %% Server = string() %% Password = string() check_password_ldap(User, Server, Password) -> {ok, State} = get_state(Server), case find_user_dn(User, Server, State) of false -> false; DN -> case eldap_pool:bind(State#state.bind_eldap_id, DN, Password) of ok -> true; _ -> false end end. %% We need an ?MODULE server state to use for queries. This will %% either be Server if this is a statically configured host or the %% Server for a different host if this is a dynamically configured %% vhost. %% The {ldap_vhost, Server} -> Host. ejabberd config option specifies %% which actual ?MODULE server to use for a particular Host. The value %% of the option if it is defined or Server by default. get_state(Server) -> Host = case ejabberd_config:get_local_option({ldap_servers, Server}) of {host, H} -> H; _ -> Server end, eldap_utils:get_state(Host, ?MODULE). %% @spec (Server) -> [{LUser, LServer}] %% Server = string() %% LUser = string() %% LServer = string() get_vh_registered_users_ldap(Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), UIDs = eldap_utils:uids_domain_subst(Server, State#state.uids), Eldap_ID = State#state.eldap_id, SearchFilter = build_sfilter(State, UIDs), ResAttrs = result_attrs(State), case eldap_filter:parse(SearchFilter) of {ok, EldapFilter} -> case eldap_pool:search(Eldap_ID, [{base, State#state.base}, {filter, EldapFilter}, {timeout, ?LDAP_SEARCH_TIMEOUT}, {attributes, ResAttrs}]) of #eldap_search_result{entries = Entries} -> lists:flatmap( fun(#eldap_entry{attributes = Attrs, object_name = DN}) -> case is_valid_dn(DN, Server, Attrs, State) of false -> []; _ -> case eldap_utils:find_ldap_attrs(UIDs, Attrs) of "" -> []; {User, UIDFormat} -> case eldap_utils:get_user_part(User, UIDFormat) of {ok, U} -> try [{exmpp_stringprep:nodeprep(U), exmpp_stringprep:nameprep(Server)}] catch _ -> [] end; _ -> [] end end end end, Entries); _ -> [] end; _ -> [] end. %% @spec (User, Server) -> bool() %% User = string() %% Server = string() is_user_exists_ldap(User, Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, Server, State) of false -> false; _DN -> true end. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> {reply, bad_request, State}. find_user_dn(User, Server, State) -> ResAttrs = result_attrs(State), UserFilter = build_ufilter(State, Server), case eldap_filter:parse(UserFilter, [{"%u", User}]) of {ok, Filter} -> case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, Filter}, {attributes, ResAttrs}]) of #eldap_search_result{entries = [#eldap_entry{attributes = Attrs, object_name = DN} | _]} -> dn_filter(DN, Server, Attrs, State); _ -> false end; _ -> false end. %% apply the dn filter and the local filter: dn_filter(DN, Server, Attrs, State) -> %% Check if user is denied access by attribute value (local check) case check_local_filter(Attrs, State) of false -> false; true -> is_valid_dn(DN, Server, Attrs, State) end. %% Check that the DN is valid, based on the dn filter is_valid_dn(DN, _, _, #state{dn_filter = undefined}) -> DN; is_valid_dn(DN, Server, Attrs, State) -> DNAttrs = State#state.dn_filter_attrs, UIDs = eldap_utils:uids_domain_subst(Server, State#state.uids), Values = [{"%s", eldap_utils:get_ldap_attr(Attr, Attrs), 1} || Attr <- DNAttrs], SubstValues = case eldap_utils:find_ldap_attrs(UIDs, Attrs) of "" -> Values; {S, UAF} -> case eldap_utils:get_user_part(S, UAF) of {ok, U} -> [{"%u", U} | Values]; _ -> Values end end ++ [{"%d", State#state.host}, {"%D", DN}], case eldap_filter:parse(State#state.dn_filter, SubstValues) of {ok, EldapFilter} -> case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, EldapFilter}, {attributes, ["dn"]}]) of #eldap_search_result{entries = [_|_]} -> DN; _ -> false end; _ -> false end. %% The local filter is used to check an attribute in ejabberd %% and not in LDAP to limit the load on the LDAP directory. %% A local rule can be either: %% {equal, {"accountStatus",["active"]}} %% {notequal, {"accountStatus",["disabled"]}} %% {ldap_local_filter, {notequal, {"accountStatus",["disabled"]}}} check_local_filter(_Attrs, #state{lfilter = undefined}) -> true; check_local_filter(Attrs, #state{lfilter = LocalFilter}) -> {Operation, FilterMatch} = LocalFilter, local_filter(Operation, Attrs, FilterMatch). local_filter(equal, Attrs, FilterMatch) -> {Attr, Value} = FilterMatch, case lists:keysearch(Attr, 1, Attrs) of false -> false; {value,{Attr,Value}} -> true; _ -> false end; local_filter(notequal, Attrs, FilterMatch) -> not local_filter(equal, Attrs, FilterMatch). result_attrs(#state{uids = UIDs, dn_filter_attrs = DNFilterAttrs}) -> lists:foldl( fun({UID}, Acc) -> [UID | Acc]; ({UID, _}, Acc) -> [UID | Acc] end, DNFilterAttrs, UIDs). build_ufilter(State, VHost) -> UIDs = eldap_utils:uids_domain_subst(VHost, State#state.uids), SubFilter = lists:flatten(eldap_utils:generate_subfilter(UIDs)), case State#state.ufilter of "" -> SubFilter; F -> "(&" ++ SubFilter ++ F ++ ")" end. build_sfilter(State, FormattedUIDs) -> SubFilter = lists:flatten(eldap_utils:generate_subfilter(FormattedUIDs)), UserFilter = case State#state.ufilter of "" -> SubFilter; F -> "(&" ++ SubFilter ++ F ++ ")" end, eldap_filter:do_sub(UserFilter, [{"%u", "*"}]). %%%---------------------------------------------------------------------- %%% Auxiliary functions %%%---------------------------------------------------------------------- parse_options(Host) -> Eldap_ID = atom_to_list(gen_mod:get_module_proc(Host, ?MODULE)), Bind_Eldap_ID = atom_to_list(gen_mod:get_module_proc(Host, bind_ejabberd_auth_ldap)), LDAPServers = ejabberd_config:get_local_option({ldap_servers, Host}), LDAPBackups = case ejabberd_config:get_local_option({ldap_backups, Host}) of undefined -> []; Backups -> Backups end, LDAPEncrypt = ejabberd_config:get_local_option({ldap_encrypt, Host}), LDAPTLSVerify = ejabberd_config:get_local_option({ldap_tls_verify, Host}), LDAPPort = 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, RootDN = case ejabberd_config:get_local_option({ldap_rootdn, Host}) of undefined -> ""; RDN -> RDN end, Password = case ejabberd_config:get_local_option({ldap_password, Host}) of undefined -> ""; Pass -> Pass end, UIDs = case ejabberd_config:get_local_option({ldap_uids, Host}) of undefined -> [{"uid", "%u"}]; UI -> UI end, UserFilter = case ejabberd_config:get_local_option({ldap_filter, Host}) of undefined -> ""; F -> F end, LDAPBase = ejabberd_config:get_local_option({ldap_base, Host}), {DNFilter, DNFilterAttrs} = case ejabberd_config:get_local_option({ldap_dn_filter, Host}) of undefined -> {undefined, []}; {DNF, undefined} -> {DNF, []}; {DNF, DNFA} -> {DNF, DNFA} end, LocalFilter = ejabberd_config:get_local_option({ldap_local_filter, Host}), #state{host = Host, eldap_id = Eldap_ID, bind_eldap_id = Bind_Eldap_ID, servers = LDAPServers, backups = LDAPBackups, port = LDAPPort, tls_options = [{encrypt, LDAPEncrypt}, {tls_verify, LDAPTLSVerify}], dn = RootDN, password = Password, base = LDAPBase, uids = UIDs, ufilter = UserFilter, lfilter = LocalFilter, dn_filter = DNFilter, dn_filter_attrs = DNFilterAttrs }.