./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("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 235 Proc = gen_mod:get_module_proc(HostType, ?MODULE),
91 235 ChildSpec = {Proc, {?MODULE, start_link, [HostType]},
92 transient, 1000, worker, [?MODULE]},
93 235 ejabberd_sup:start_child(ChildSpec),
94 235 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 160 #section{
106 items = #{<<"pool_tag">> => #option{type = atom,
107 validate = non_empty},
108 <<"bind_pool_tag">> => #option{type = atom,
109 validate = non_empty},
110 <<"base">> => #option{type = binary},
111 <<"uids">> => #list{items = mongoose_ldap_config:uids()},
112 <<"filter">> => #option{type = binary,
113 validate = ldap_filter},
114 <<"dn_filter">> => mongoose_ldap_config:dn_filter(),
115 <<"local_filter">> => mongoose_ldap_config:local_filter(),
116 <<"deref">> => #option{type = atom,
117 validate = {enum, [never, always, finding, searching]}}
118 },
119 defaults = #{<<"pool_tag">> => default,
120 <<"bind_pool_tag">> => bind,
121 <<"base">> => <<>>,
122 <<"uids">> => [{<<"uid">>, <<"%u">>}],
123 <<"filter">> => <<>>,
124 <<"dn_filter">> => {undefined, []},
125 <<"local_filter">> => undefined,
126 <<"deref">> => never},
127 format_items = map
128 }.
129
130 -spec start_link(HostType :: mongooseim:host_type()) -> {ok, pid()} | {error, any()}.
131 start_link(HostType) ->
132 235 Proc = gen_mod:get_module_proc(HostType, ?MODULE),
133 235 gen_server:start_link({local, Proc}, ?MODULE, HostType, []).
134
135 1 terminate(_Reason, _State) -> ok.
136
137 -spec init(HostType :: mongooseim:host_type()) -> {'ok', state()}.
138 init(HostType) ->
139 235 State = parse_options(HostType),
140 235 {ok, State}.
141
142 -spec supports_sasl_module(mongooseim:host_type(), cyrsasl:sasl_module()) -> boolean().
143 8042 supports_sasl_module(_, cyrsasl_plain) -> true;
144 27 supports_sasl_module(_, cyrsasl_external) -> true;
145 60017 supports_sasl_module(_, _) -> false.
146
147 -spec authorize(mongoose_credentials:t()) -> {ok, mongoose_credentials:t()}
148 | {error, any()}.
149 authorize(Creds) ->
150 2740 case mongoose_credentials:get(Creds, cert_file, false) of
151 6 true -> verify_user_exists(Creds);
152 2734 false -> ejabberd_auth:authorize_with_check_password(?MODULE, Creds)
153 end.
154
155 -spec check_password(HostType :: mongooseim:host_type(),
156 LUser :: jid:luser(),
157 LServer :: jid:lserver(),
158 Password :: binary()) -> boolean().
159
:-(
check_password(_HostType, _LUser, _LServer, <<"">>) -> false;
160 check_password(HostType, LUser, LServer, Password) ->
161 2738 case catch check_password_ldap(HostType, LUser, LServer, Password) of
162
:-(
{'EXIT', _} -> false;
163 2738 Result -> Result
164 end.
165
166 -spec check_password(HostType :: mongooseim:host_type(),
167 LUser :: jid:luser(),
168 LServer :: jid:lserver(),
169 Password :: binary(),
170 Digest :: binary(),
171 DigestGen :: fun()) -> boolean().
172 check_password(HostType, LUser, LServer, Password, _Digest,
173 _DigestGen) ->
174
:-(
check_password(HostType, LUser, LServer, Password).
175
176
177 -spec set_password(HostType :: mongooseim:host_type(),
178 LUser :: jid:luser(),
179 LServer :: jid:lserver(),
180 Password :: binary())
181 -> ok | {error, not_allowed | invalid_jid | user_not_found}.
182 set_password(HostType, LUser, LServer, Password) ->
183 21 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
184 21 case find_user_dn(LUser, LServer, State) of
185
:-(
false -> {error, user_not_found};
186 DN ->
187 21 eldap_pool:modify_passwd(State#state.eldap_id, DN, Password)
188 end.
189
190 %% TODO Support multiple domains
191 -spec try_register(HostType :: mongooseim:host_type(), LUser :: jid:luser(),
192 LServer :: jid:lserver(), Password :: binary()) ->
193 ok | {error, exists}.
194 try_register(HostType, LUser, _LServer, Password) ->
195 2206 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
196 2206 UserStr = binary_to_list(LUser),
197 2206 DN = "cn=" ++ UserStr ++ ", " ++ binary_to_list(State#state.base),
198 2206 Attrs = [{"objectclass", ["inetOrgPerson"]},
199 {"cn", [UserStr]},
200 {"sn", [UserStr]},
201 {"userPassword", [binary_to_list(Password)]},
202 {"uid", [UserStr]}],
203 2206 case eldap_pool:add(State#state.eldap_id, DN, Attrs) of
204 2206 ok -> ok;
205
:-(
_ -> {error, exists}
206 end.
207
208 -spec get_registered_users(HostType :: mongooseim:host_type(),
209 LServer :: jid:lserver(),
210 Opts :: list()) -> [jid:simple_bare_jid()].
211 get_registered_users(HostType, LServer, _) ->
212 364 case catch get_registered_users_ldap(HostType, LServer) of
213
:-(
{'EXIT', _} -> [];
214 364 Result -> Result
215 end.
216
217
218 -spec get_registered_users_number(HostType :: mongooseim:host_type(),
219 LServer :: jid:lserver(),
220 Opts :: list()) -> non_neg_integer().
221 get_registered_users_number(HostType, LServer, Opts) ->
222 153 length(get_registered_users(HostType, LServer, Opts)).
223
224
225 -spec does_user_exist(HostType :: mongooseim:host_type(),
226 LUser :: jid:luser(),
227 LServer :: jid:lserver()) -> boolean() | {error, atom()}.
228 does_user_exist(HostType, LUser, LServer) ->
229 2934 case catch does_user_exist_in_ldap(HostType, LUser, LServer) of
230
:-(
{'EXIT', Error} -> {error, Error};
231 2934 Result -> Result
232 end.
233
234
235 -spec remove_user(HostType :: mongooseim:host_type(),
236 LUser :: jid:luser(),
237 LServer :: jid:lserver()) -> ok | {error, not_allowed}.
238 remove_user(HostType, LUser, LServer) ->
239 2258 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
240 2258 case find_user_dn(LUser, LServer, State) of
241 54 false -> {error, not_allowed};
242 2204 DN -> eldap_pool:delete(State#state.eldap_id, DN)
243 end.
244
245 %% Multiple domains are not supported for in-band registration
246 -spec supported_features() -> [atom()].
247 56 supported_features() -> [dynamic_domains].
248
249 %%%----------------------------------------------------------------------
250 %%% Internal functions
251 %%%----------------------------------------------------------------------
252
253 -spec verify_user_exists(mongoose_credentials:t()) ->
254 {ok, mongoose_credentials:t()} | {error, not_authorized}.
255 verify_user_exists(Creds) ->
256 6 User = mongoose_credentials:get(Creds, username),
257 6 case jid:nodeprep(User) of
258 error ->
259
:-(
error({nodeprep_error, User});
260 LUser ->
261 6 LServer = mongoose_credentials:lserver(Creds),
262 6 HostType = mongoose_credentials:host_type(Creds),
263 6 case does_user_exist(HostType, LUser, LServer) of
264 3 true -> {ok, mongoose_credentials:extend(Creds, [{auth_module, ?MODULE}])};
265 3 false -> {error, not_authorized}
266 end
267 end.
268
269 -spec check_password_ldap(HostType :: mongooseim:host_type(),
270 LUser :: jid:luser(),
271 LServer :: jid:lserver(),
272 Password :: binary()) -> boolean().
273 check_password_ldap(HostType, LUser, LServer, Password) ->
274 2738 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
275 2738 case find_user_dn(LUser, LServer, State) of
276 10 false -> false;
277 DN ->
278 2728 case eldap_pool:bind(State#state.bind_eldap_id, DN, Password) of
279 2722 ok -> true;
280 6 _ -> false
281 end
282 end.
283
284
285 -spec get_registered_users_ldap(mongooseim:host_type(), jid:lserver()) -> [jid:simple_bare_jid()].
286 get_registered_users_ldap(HostType, LServer) ->
287 364 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
288 364 UIDs = State#state.uids,
289 364 EldapID = State#state.eldap_id,
290 364 ResAttrs = result_attrs(State),
291 364 case eldap_filter:parse(State#state.sfilter) of
292 {ok, EldapFilter} ->
293 364 case eldap_pool:search(EldapID,
294 [{base, State#state.base},
295 {filter, EldapFilter},
296 {timeout, ?LDAP_SEARCH_TIMEOUT},
297 {deref, State#state.deref},
298 {attributes, ResAttrs}]) of
299 #eldap_search_result{entries = Entries} ->
300 364 get_users_from_ldap_entries(Entries, UIDs, LServer, State);
301
:-(
_ -> []
302 end;
303
:-(
_ -> []
304 end.
305
306 -spec get_users_from_ldap_entries(list(), [{binary()} | {binary(), binary()}],
307 jid:lserver(), state()) -> list().
308 get_users_from_ldap_entries(LDAPEntries, UIDs, LServer, State) ->
309 364 lists:flatmap(
310 fun(#eldap_entry{attributes = Attrs,
311 object_name = DN}) ->
312 1041 case is_valid_dn(DN, LServer, Attrs, State) of
313
:-(
false -> [];
314 true ->
315 1041 get_user_from_ldap_attributes(UIDs, Attrs, LServer)
316 end
317 end,
318 LDAPEntries).
319
320 -spec get_user_from_ldap_attributes([{binary()} | {binary(), binary()}],
321 [{binary(), [binary()]}], jid:lserver())
322 -> list().
323 get_user_from_ldap_attributes(UIDs, Attributes, LServer) ->
324 1041 case eldap_utils:find_ldap_attrs(UIDs, Attributes) of
325
:-(
<<"">> -> [];
326 {User, UIDFormat} ->
327 1041 case eldap_utils:get_user_part(User, UIDFormat) of
328 {ok, U} ->
329 1041 [{U, LServer}];
330
:-(
_ -> []
331 end
332 end.
333
334 -spec does_user_exist_in_ldap(HostType :: mongooseim:host_type(),
335 LUser :: jid:luser(),
336 LServer :: jid:lserver()) -> boolean().
337 does_user_exist_in_ldap(HostType, LUser, LServer) ->
338 2934 {ok, State} = eldap_utils:get_state(HostType, ?MODULE),
339 2934 case find_user_dn(LUser, LServer, State) of
340 2320 false -> false;
341 614 _DN -> true
342 end.
343
344 handle_call(get_state, _From, State) ->
345 10521 {reply, {ok, State}, State};
346 handle_call(stop, _From, State) ->
347 1 {stop, normal, ok, State};
348 handle_call(_Request, _From, State) ->
349
:-(
{reply, bad_request, State}.
350
351 -spec find_user_dn(LUser :: jid:luser(),
352 LServer :: jid:lserver(),
353 State :: state()) -> false | eldap_utils:dn().
354 find_user_dn(LUser, LServer, State) ->
355 7951 ResAttrs = result_attrs(State),
356 7951 case eldap_filter:parse(State#state.ufilter, [{<<"%u">>, LUser}]) of
357 {ok, Filter} ->
358 7951 SearchOpts = find_user_opts(Filter, ResAttrs, State),
359 7951 case eldap_pool:search(State#state.eldap_id, SearchOpts) of
360 #eldap_search_result{entries =
361 [#eldap_entry{attributes = Attrs,
362 object_name = DN}
363 | _]} ->
364 5567 dn_filter(DN, LServer, Attrs, State);
365 2384 _ -> false
366 end;
367
:-(
_ -> false
368 end.
369
370 find_user_opts(Filter, ResAttrs, State) ->
371 7951 [{base, State#state.base}, {filter, Filter},
372 {deref, State#state.deref}, {attributes, ResAttrs}].
373
374
375 %% @doc apply the dn filter and the local filter:
376 -spec dn_filter(DN :: eldap_utils:dn(),
377 LServer :: jid:lserver(),
378 Attrs :: [{binary(), [any()]}],
379 State :: state()) -> false | eldap_utils:dn().
380 dn_filter(DN, LServer, Attrs, State) ->
381 5567 case check_local_filter(Attrs, State) of
382
:-(
false -> false;
383 true ->
384 5567 case is_valid_dn(DN, LServer, Attrs, State) of
385 5567 true -> DN;
386
:-(
false -> false
387 end
388 end.
389
390 %% @doc Check that the DN is valid, based on the dn filter
391 -spec is_valid_dn(DN :: eldap_utils:dn(),
392 LServer :: jid:lserver(),
393 Attrs :: [{binary(), [any()]}],
394 State :: state()) -> boolean().
395 6608 is_valid_dn(_DN, _LServer, _, #state{dn_filter = undefined}) -> true;
396 is_valid_dn(DN, LServer, Attrs, State) ->
397
:-(
DNAttrs = State#state.dn_filter_attrs,
398
:-(
UIDs = State#state.uids,
399
:-(
Values = [{<<"%s">>, eldap_utils:get_ldap_attr(Attr, Attrs), 1}
400
:-(
|| Attr <- DNAttrs],
401
:-(
SubstValues = case eldap_utils:find_ldap_attrs(UIDs, Attrs) of
402
:-(
<<>> -> Values;
403 {S, UAF} ->
404
:-(
case eldap_utils:get_user_part(S, UAF) of
405
:-(
{ok, U} -> [{<<"%u">>, U} | Values];
406
:-(
_ -> Values
407 end
408 end ++ [{<<"%d">>, LServer}, {<<"%D">>, DN}],
409
:-(
case eldap_filter:parse(State#state.dn_filter, SubstValues) of
410 {ok, EldapFilter} ->
411
:-(
case eldap_pool:search(State#state.eldap_id,
412 [{base, State#state.base},
413 {filter, EldapFilter},
414 {deref, State#state.deref},
415 {attributes, [<<"dn">>]}])
416 of
417
:-(
#eldap_search_result{entries = [_ | _]} -> true;
418
:-(
_ -> false
419 end;
420
:-(
_ -> false
421 end.
422
423
424 %% @doc The local filter is used to check an attribute in ejabberd
425 %% and not in LDAP to limit the load on the LDAP directory.
426 %% A local rule can be either:
427 %% {equal, {"accountStatus", ["active"]}}
428 %% {notequal, {"accountStatus", ["disabled"]}}
429 %% {ldap_local_filter, {notequal, {"accountStatus", ["disabled"]}}}
430 -spec check_local_filter(Attrs :: [{binary(), [any()]}],
431 State :: state()) -> boolean().
432 check_local_filter(_Attrs,
433 #state{lfilter = undefined}) ->
434 5567 true;
435 check_local_filter(Attrs,
436 #state{lfilter = LocalFilter}) ->
437
:-(
{Operation, FilterMatch} = LocalFilter,
438
:-(
local_filter(Operation, Attrs, FilterMatch).
439
440
441 -spec local_filter('equal' | 'notequal',
442 Attrs :: [{binary(), [any()]}],
443 FilterMatch :: {_, _}) -> boolean().
444 local_filter(equal, Attrs, FilterMatch) ->
445
:-(
{Attr, Value} = FilterMatch,
446
:-(
case lists:keysearch(Attr, 1, Attrs) of
447
:-(
false -> false;
448
:-(
{value, {Attr, Value}} -> true;
449
:-(
_ -> false
450 end;
451 local_filter(notequal, Attrs, FilterMatch) ->
452
:-(
not local_filter(equal, Attrs, FilterMatch).
453
454
455 -spec result_attrs(state()) -> maybe_improper_list().
456 result_attrs(#state{uids = UIDs,
457 dn_filter_attrs = DNFilterAttrs}) ->
458 8315 lists:foldl(fun ({UID}, Acc) -> [UID | Acc];
459 8315 ({UID, _}, Acc) -> [UID | Acc]
460 end,
461 DNFilterAttrs, UIDs).
462
463 %%%----------------------------------------------------------------------
464 %%% Auxiliary functions
465 %%%----------------------------------------------------------------------
466
467 -spec parse_options(HostType :: mongooseim:host_type()) -> state().
468 parse_options(HostType) ->
469 235 Opts = mongoose_config:get_opt([{auth, HostType}, ldap]),
470 235 EldapID = {HostType, maps:get(pool_tag, Opts)},
471 235 BindEldapID = {HostType, maps:get(bind_pool_tag, Opts)},
472 235 Base = maps:get(base, Opts),
473 235 DerefAliases = eldap_utils:deref_aliases(maps:get(deref, Opts)),
474 235 RawUIDs = maps:get(uids, Opts),
475 235 UIDs = eldap_utils:uids_domain_subst(HostType, RawUIDs),
476 235 RawUserFilter = maps:get(filter, Opts),
477 235 UserFilter = eldap_utils:process_user_filter(UIDs, RawUserFilter),
478 235 SearchFilter = eldap_utils:get_search_filter(UserFilter),
479 235 {DNFilter, DNFilterAttrs} = maps:get(dn_filter, Opts),
480 235 LocalFilter = maps:get(local_filter, Opts),
481 235 #state{host_type = HostType,
482 eldap_id = EldapID,
483 bind_eldap_id = BindEldapID,
484 base = Base,
485 deref = DerefAliases,
486 uids = UIDs,
487 ufilter = UserFilter,
488 sfilter = SearchFilter,
489 lfilter = LocalFilter,
490 dn_filter = DNFilter,
491 dn_filter_attrs = DNFilterAttrs}.
Line Hits Source