./ct_report/coverage/ejabberd_auth_ldap.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : ejabberd_auth_ldap.erl
3 %%% Author : Alexey Shchepin <alexey@process-one.net>
4 %%% Purpose : Authentification via LDAP
5 %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net>
6 %%%
7 %%%
8 %%% ejabberd, Copyright (C) 2002-2013 ProcessOne
9 %%%
10 %%% This program is free software; you can redistribute it and/or
11 %%% modify it under the terms of the GNU General Public License as
12 %%% published by the Free Software Foundation; either version 2 of the
13 %%% License, or (at your option) any later version.
14 %%%
15 %%% This program is distributed in the hope that it will be useful,
16 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 %%% General Public License for more details.
19 %%%
20 %%% You should have received a copy of the GNU General Public License
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 %%%
24 %%%----------------------------------------------------------------------
25
26 -module(ejabberd_auth_ldap).
27 -author('alexey@process-one.net').
28
29 %% gen_server callbacks
30 -behaviour(gen_server).
31 -export([init/1, handle_info/2, handle_call/3,
32 handle_cast/2, terminate/2, code_change/3]).
33
34 %% External exports
35 -behaviour(mongoose_gen_auth).
36
37 -export([start/1,
38 stop/1,
39 config_spec/0,
40 start_link/1,
41 set_password/4,
42 authorize/1,
43 try_register/4,
44 get_registered_users/3,
45 get_registered_users_number/3,
46 does_user_exist/3,
47 remove_user/3,
48 supports_sasl_module/2,
49 supported_features/0
50 ]).
51
52 %% Internal
53 -export([check_password/4,
54 check_password/6]).
55
56 -ignore_xref([start_link/1]).
57
58 -include("mongoose_config_spec.hrl").
59 -include_lib("eldap/include/eldap.hrl").
60
61 -record(state,
62 {host_type :: mongooseim:host_type(),
63 eldap_id :: eldap_utils:eldap_id(),
64 bind_eldap_id :: eldap_utils:eldap_id(),
65 base = <<>> :: binary(),
66 uids = [] :: [{binary()} | {binary(), binary()}],
67 ufilter = <<>> :: binary(),
68 sfilter = <<>> :: binary(),
69 lfilter :: {any(), any()} | undefined,
70 deref = neverDerefAliases :: eldap_utils:deref(),
71 dn_filter :: eldap_utils:dn() | undefined,
72 dn_filter_attrs = [] :: [binary()]
73 }).
74 -type state() :: #state{}.
75
76
:-(
handle_cast(_Request, State) -> {noreply, State}.
77
78
:-(
code_change(_OldVsn, State, _Extra) -> {ok, State}.
79
80
:-(
handle_info(_Info, State) -> {noreply, State}.
81
82 -define(LDAP_SEARCH_TIMEOUT, 5).
83
84 %%%----------------------------------------------------------------------
85 %%% API
86 %%%----------------------------------------------------------------------
87
88 -spec start(HostType :: mongooseim:host_type()) -> ok.
89 start(HostType) ->
90 254 Proc = gen_mod:get_module_proc(HostType, ?MODULE),
91 254 ChildSpec = {Proc, {?MODULE, start_link, [HostType]},
92 transient, 1000, worker, [?MODULE]},
93 254 ejabberd_sup:start_child(ChildSpec),
94 254 ok.
95
96 -spec stop(HostType :: mongooseim:host_type()) -> ok.
97 stop(HostType) ->
98 1 Proc = gen_mod:get_module_proc(HostType, ?MODULE),
99 1 gen_server:call(Proc, stop),
100 1 ejabberd_sup:stop_child(Proc),
101 1 ok.
102
103 -spec config_spec() -> mongoose_config_spec:config_section().
104 config_spec() ->
105 202 CommonLDAPSpec = mongoose_ldap_config:spec(),
106 202 Items = #{<<"bind_pool_tag">> => #option{type = atom,
107 validate = non_empty},
108 <<"uids">> => #list{items = mongoose_ldap_config:uids()},
109 <<"dn_filter">> => mongoose_ldap_config:dn_filter(),
110 <<"local_filter">> => mongoose_ldap_config:local_filter()},
111 202 Defaults = #{<<"bind_pool_tag">> => bind,
112 <<"base">> => <<>>,
113 <<"uids">> => [{<<"uid">>, <<"%u">>}],
114 <<"dn_filter">> => {undefined, []},
115 <<"local_filter">> => undefined},
116 202 CommonLDAPSpec#section{items = maps:merge(CommonLDAPSpec#section.items, Items),
117 defaults = maps:merge(CommonLDAPSpec#section.defaults, Defaults)}.
118
119 -spec start_link(HostType :: mongooseim:host_type()) -> {ok, pid()} | {error, any()}.
120 start_link(HostType) ->
121 254 Proc = gen_mod:get_module_proc(HostType, ?MODULE),
122 254 gen_server:start_link({local, Proc}, ?MODULE, HostType, []).
123
124 1 terminate(_Reason, _State) -> ok.
125
126 -spec init(HostType :: mongooseim:host_type()) -> {'ok', state()}.
127 init(HostType) ->
128 254 State = parse_options(HostType),
129 254 {ok, State}.
130
131 -spec supports_sasl_module(mongooseim:host_type(), cyrsasl:sasl_module()) -> boolean().
132 9667 supports_sasl_module(_, cyrsasl_plain) -> true;
133 27 supports_sasl_module(_, cyrsasl_external) -> true;
134 71866 supports_sasl_module(_, _) -> false.
135
136 -spec authorize(mongoose_credentials:t()) -> {ok, mongoose_credentials:t()}
137 | {error, any()}.
138 authorize(Creds) ->
139 3487 case mongoose_credentials:get(Creds, cert_file, false) of
140 6 true -> verify_user_exists(Creds);
141 3481 false -> ejabberd_auth:authorize_with_check_password(?MODULE, Creds)
142 end.
143
144 -spec check_password(HostType :: mongooseim:host_type(),
145 LUser :: jid:luser(),
146 LServer :: jid:lserver(),
147 Password :: binary()) -> boolean().
148
:-(
check_password(_HostType, _LUser, _LServer, <<"">>) -> false;
149 check_password(HostType, LUser, LServer, Password) ->
150 3485 case catch check_password_ldap(HostType, LUser, LServer, Password) of
151
:-(
{'EXIT', _} -> false;
152 3485 Result -> Result
153 end.
154
155 -spec check_password(HostType :: mongooseim:host_type(),
156 LUser :: jid:luser(),
157 LServer :: jid:lserver(),
158 Password :: binary(),
159 Digest :: binary(),
160 DigestGen :: fun()) -> boolean().
161 check_password(HostType, LUser, LServer, Password, _Digest,
162 _DigestGen) ->
163
:-(
check_password(HostType, LUser, LServer, Password).
164
165
166 -spec set_password(HostType :: mongooseim:host_type(),
167 LUser :: jid:luser(),
168 LServer :: jid:lserver(),
169 Password :: binary())
170 -> ok | {error, not_allowed | invalid_jid | user_not_found}.
171 set_password(HostType, LUser, LServer, Password) ->
172 9 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
173 9 case find_user_dn(LUser, LServer, State) of
174
:-(
false -> {error, user_not_found};
175 DN ->
176 9 eldap_pool:modify_passwd(State#state.eldap_id, DN, Password)
177 end.
178
179 %% TODO Support multiple domains
180 -spec try_register(HostType :: mongooseim:host_type(), LUser :: jid:luser(),
181 LServer :: jid:lserver(), Password :: binary()) ->
182 ok | {error, exists}.
183 try_register(HostType, LUser, _LServer, Password) ->
184 2823 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
185 2823 UserStr = binary_to_list(LUser),
186 2823 DN = "cn=" ++ UserStr ++ ", " ++ binary_to_list(State#state.base),
187 2823 Attrs = [{"objectclass", ["inetOrgPerson"]},
188 {"cn", [UserStr]},
189 {"sn", [UserStr]},
190 {"userPassword", [binary_to_list(Password)]},
191 {"uid", [UserStr]}],
192 2823 case eldap_pool:add(State#state.eldap_id, DN, Attrs) of
193 2823 ok -> ok;
194
:-(
_ -> {error, exists}
195 end.
196
197 -spec get_registered_users(HostType :: mongooseim:host_type(),
198 LServer :: jid:lserver(),
199 Opts :: list()) -> [jid:simple_bare_jid()].
200 get_registered_users(HostType, LServer, _) ->
201 3239 case catch get_registered_users_ldap(HostType, LServer) of
202
:-(
{'EXIT', _} -> [];
203 3239 Result -> Result
204 end.
205
206
207 -spec get_registered_users_number(HostType :: mongooseim:host_type(),
208 LServer :: jid:lserver(),
209 Opts :: list()) -> non_neg_integer().
210 get_registered_users_number(HostType, LServer, Opts) ->
211 3038 length(get_registered_users(HostType, LServer, Opts)).
212
213
214 -spec does_user_exist(HostType :: mongooseim:host_type(),
215 LUser :: jid:luser(),
216 LServer :: jid:lserver()) -> boolean() | {error, atom()}.
217 does_user_exist(HostType, LUser, LServer) ->
218 7230 case catch does_user_exist_in_ldap(HostType, LUser, LServer) of
219
:-(
{'EXIT', Error} -> {error, Error};
220 7230 Result -> Result
221 end.
222
223
224 -spec remove_user(HostType :: mongooseim:host_type(),
225 LUser :: jid:luser(),
226 LServer :: jid:lserver()) -> ok | {error, not_allowed}.
227 remove_user(HostType, LUser, LServer) ->
228 2826 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
229 2826 case find_user_dn(LUser, LServer, State) of
230
:-(
false -> {error, not_allowed};
231 2826 DN -> eldap_pool:delete(State#state.eldap_id, DN)
232 end.
233
234 %% Multiple domains are not supported for in-band registration
235 -spec supported_features() -> [atom()].
236 83 supported_features() -> [dynamic_domains].
237
238 %%%----------------------------------------------------------------------
239 %%% Internal functions
240 %%%----------------------------------------------------------------------
241
242 -spec verify_user_exists(mongoose_credentials:t()) ->
243 {ok, mongoose_credentials:t()} | {error, not_authorized}.
244 verify_user_exists(Creds) ->
245 6 User = mongoose_credentials:get(Creds, username),
246 6 case jid:nodeprep(User) of
247 error ->
248
:-(
error({nodeprep_error, User});
249 LUser ->
250 6 LServer = mongoose_credentials:lserver(Creds),
251 6 HostType = mongoose_credentials:host_type(Creds),
252 6 case does_user_exist(HostType, LUser, LServer) of
253 3 true -> {ok, mongoose_credentials:extend(Creds, [{auth_module, ?MODULE}])};
254 3 false -> {error, not_authorized}
255 end
256 end.
257
258 -spec check_password_ldap(HostType :: mongooseim:host_type(),
259 LUser :: jid:luser(),
260 LServer :: jid:lserver(),
261 Password :: binary()) -> boolean().
262 check_password_ldap(HostType, LUser, LServer, Password) ->
263 3485 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
264 3485 case find_user_dn(LUser, LServer, State) of
265 13 false -> false;
266 DN ->
267 3472 case eldap_pool:bind(State#state.bind_eldap_id, DN, Password) of
268 3468 ok -> true;
269 4 _ -> false
270 end
271 end.
272
273
274 -spec get_registered_users_ldap(mongooseim:host_type(), jid:lserver()) -> [jid:simple_bare_jid()].
275 get_registered_users_ldap(HostType, LServer) ->
276 3239 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
277 3239 UIDs = State#state.uids,
278 3239 EldapID = State#state.eldap_id,
279 3239 ResAttrs = result_attrs(State),
280 3239 case eldap_filter:parse(State#state.sfilter) of
281 {ok, EldapFilter} ->
282 3239 case eldap_pool:search(EldapID,
283 [{base, State#state.base},
284 {filter, EldapFilter},
285 {timeout, ?LDAP_SEARCH_TIMEOUT},
286 {deref, State#state.deref},
287 {attributes, ResAttrs}]) of
288 #eldap_search_result{entries = Entries} ->
289 3239 get_users_from_ldap_entries(Entries, UIDs, LServer, State);
290
:-(
_ -> []
291 end;
292
:-(
_ -> []
293 end.
294
295 -spec get_users_from_ldap_entries(list(), [{binary()} | {binary(), binary()}],
296 jid:lserver(), state()) -> list().
297 get_users_from_ldap_entries(LDAPEntries, UIDs, LServer, State) ->
298 3239 lists:flatmap(
299 fun(#eldap_entry{attributes = Attrs,
300 object_name = DN}) ->
301 169817 case is_valid_dn(DN, LServer, Attrs, State) of
302
:-(
false -> [];
303 true ->
304 169817 get_user_from_ldap_attributes(UIDs, Attrs, LServer)
305 end
306 end,
307 LDAPEntries).
308
309 -spec get_user_from_ldap_attributes([{binary()} | {binary(), binary()}],
310 [{binary(), [binary()]}], jid:lserver())
311 -> list().
312 get_user_from_ldap_attributes(UIDs, Attributes, LServer) ->
313 169817 case eldap_utils:find_ldap_attrs(UIDs, Attributes) of
314
:-(
<<"">> -> [];
315 {User, UIDFormat} ->
316 169817 case eldap_utils:get_user_part(User, UIDFormat) of
317 {ok, U} ->
318 169817 [{U, LServer}];
319
:-(
_ -> []
320 end
321 end.
322
323 -spec does_user_exist_in_ldap(HostType :: mongooseim:host_type(),
324 LUser :: jid:luser(),
325 LServer :: jid:lserver()) -> boolean().
326 does_user_exist_in_ldap(HostType, LUser, LServer) ->
327 7230 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
328 7230 case find_user_dn(LUser, LServer, State) of
329 3145 false -> false;
330 4085 _DN -> true
331 end.
332
333 handle_call(get_state, _From, State) ->
334 19612 {reply, {ok, State}, State};
335 handle_call(stop, _From, State) ->
336 1 {stop, normal, ok, State};
337 handle_call(_Request, _From, State) ->
338
:-(
{reply, bad_request, State}.
339
340 -spec find_user_dn(LUser :: jid:luser(),
341 LServer :: jid:lserver(),
342 State :: state()) -> false | eldap_utils:dn().
343 find_user_dn(LUser, LServer, State) ->
344 13550 ResAttrs = result_attrs(State),
345 13550 case eldap_filter:parse(State#state.ufilter, [{<<"%u">>, LUser}]) of
346 {ok, Filter} ->
347 13550 SearchOpts = find_user_opts(Filter, ResAttrs, State),
348 13550 case eldap_pool:search(State#state.eldap_id, SearchOpts) of
349 #eldap_search_result{entries =
350 [#eldap_entry{attributes = Attrs,
351 object_name = DN}
352 | _]} ->
353 10392 dn_filter(DN, LServer, Attrs, State);
354 3158 _ -> false
355 end;
356
:-(
_ -> false
357 end.
358
359 find_user_opts(Filter, ResAttrs, State) ->
360 13550 [{base, State#state.base}, {filter, Filter},
361 {deref, State#state.deref}, {attributes, ResAttrs}].
362
363
364 %% @doc apply the dn filter and the local filter:
365 -spec dn_filter(DN :: eldap_utils:dn(),
366 LServer :: jid:lserver(),
367 Attrs :: [{binary(), [any()]}],
368 State :: state()) -> false | eldap_utils:dn().
369 dn_filter(DN, LServer, Attrs, State) ->
370 10392 case check_local_filter(Attrs, State) of
371
:-(
false -> false;
372 true ->
373 10392 case is_valid_dn(DN, LServer, Attrs, State) of
374 10392 true -> DN;
375
:-(
false -> false
376 end
377 end.
378
379 %% @doc Check that the DN is valid, based on the dn filter
380 -spec is_valid_dn(DN :: eldap_utils:dn(),
381 LServer :: jid:lserver(),
382 Attrs :: [{binary(), [any()]}],
383 State :: state()) -> boolean().
384 180209 is_valid_dn(_DN, _LServer, _, #state{dn_filter = undefined}) -> true;
385 is_valid_dn(DN, LServer, Attrs, State) ->
386
:-(
DNAttrs = State#state.dn_filter_attrs,
387
:-(
UIDs = State#state.uids,
388
:-(
Values = [{<<"%s">>, eldap_utils:get_ldap_attr(Attr, Attrs), 1}
389
:-(
|| Attr <- DNAttrs],
390
:-(
SubstValues = case eldap_utils:find_ldap_attrs(UIDs, Attrs) of
391
:-(
<<>> -> Values;
392 {S, UAF} ->
393
:-(
case eldap_utils:get_user_part(S, UAF) of
394
:-(
{ok, U} -> [{<<"%u">>, U} | Values];
395
:-(
_ -> Values
396 end
397 end ++ [{<<"%d">>, LServer}, {<<"%D">>, DN}],
398
:-(
case eldap_filter:parse(State#state.dn_filter, SubstValues) of
399 {ok, EldapFilter} ->
400
:-(
case eldap_pool:search(State#state.eldap_id,
401 [{base, State#state.base},
402 {filter, EldapFilter},
403 {deref, State#state.deref},
404 {attributes, [<<"dn">>]}])
405 of
406
:-(
#eldap_search_result{entries = [_ | _]} -> true;
407
:-(
_ -> false
408 end;
409
:-(
_ -> false
410 end.
411
412
413 %% @doc The local filter is used to check an attribute in ejabberd
414 %% and not in LDAP to limit the load on the LDAP directory.
415 %% A local rule can be either:
416 %% {equal, {"accountStatus", ["active"]}}
417 %% {notequal, {"accountStatus", ["disabled"]}}
418 %% {ldap_local_filter, {notequal, {"accountStatus", ["disabled"]}}}
419 -spec check_local_filter(Attrs :: [{binary(), [any()]}],
420 State :: state()) -> boolean().
421 check_local_filter(_Attrs,
422 #state{lfilter = undefined}) ->
423 10392 true;
424 check_local_filter(Attrs,
425 #state{lfilter = LocalFilter}) ->
426
:-(
{Operation, FilterMatch} = LocalFilter,
427
:-(
local_filter(Operation, Attrs, FilterMatch).
428
429
430 -spec local_filter('equal' | 'notequal',
431 Attrs :: [{binary(), [any()]}],
432 FilterMatch :: {_, _}) -> boolean().
433 local_filter(equal, Attrs, FilterMatch) ->
434
:-(
{Attr, Value} = FilterMatch,
435
:-(
case lists:keysearch(Attr, 1, Attrs) of
436
:-(
false -> false;
437
:-(
{value, {Attr, Value}} -> true;
438
:-(
_ -> false
439 end;
440 local_filter(notequal, Attrs, FilterMatch) ->
441
:-(
not local_filter(equal, Attrs, FilterMatch).
442
443
444 -spec result_attrs(state()) -> maybe_improper_list().
445 result_attrs(#state{uids = UIDs,
446 dn_filter_attrs = DNFilterAttrs}) ->
447 16789 lists:foldl(fun ({UID}, Acc) -> [UID | Acc];
448 16789 ({UID, _}, Acc) -> [UID | Acc]
449 end,
450 DNFilterAttrs, UIDs).
451
452 %%%----------------------------------------------------------------------
453 %%% Auxiliary functions
454 %%%----------------------------------------------------------------------
455
456 -spec parse_options(HostType :: mongooseim:host_type()) -> state().
457 parse_options(HostType) ->
458 254 Opts = mongoose_config:get_opt([{auth, HostType}, ldap]),
459 254 #{pool_tag := PoolTag,
460 bind_pool_tag := BindPoolTag,
461 base := Base,
462 deref := Deref,
463 uids := RawUIDs,
464 filter := RawUserFilter,
465 dn_filter := {DNFilter, DNFilterAttrs},
466 local_filter := LocalFilter} = Opts,
467 254 EldapID = {HostType, PoolTag},
468 254 BindEldapID = {HostType, BindPoolTag},
469 254 DerefAliases = eldap_utils:deref_aliases(Deref),
470 254 UIDs = eldap_utils:uids_domain_subst(HostType, RawUIDs),
471 254 UserFilter = eldap_utils:process_user_filter(UIDs, RawUserFilter),
472 254 SearchFilter = eldap_utils:get_search_filter(UserFilter),
473 254 #state{host_type = HostType,
474 eldap_id = EldapID,
475 bind_eldap_id = BindEldapID,
476 base = Base,
477 deref = DerefAliases,
478 uids = UIDs,
479 ufilter = UserFilter,
480 sfilter = SearchFilter,
481 lfilter = LocalFilter,
482 dn_filter = DNFilter,
483 dn_filter_attrs = DNFilterAttrs}.
Line Hits Source