./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
:-(
Proc = gen_mod:get_module_proc(HostType, ?MODULE),
91
:-(
ChildSpec = {Proc, {?MODULE, start_link, [HostType]},
92 transient, 1000, worker, [?MODULE]},
93
:-(
ejabberd_sup:start_child(ChildSpec),
94
:-(
ok.
95
96 -spec stop(HostType :: mongooseim:host_type()) -> ok.
97 stop(HostType) ->
98
:-(
Proc = gen_mod:get_module_proc(HostType, ?MODULE),
99
:-(
gen_server:call(Proc, stop),
100
:-(
ejabberd_sup:stop_child(Proc),
101
:-(
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
:-(
Proc = gen_mod:get_module_proc(HostType, ?MODULE),
133
:-(
gen_server:start_link({local, Proc}, ?MODULE, HostType, []).
134
135
:-(
terminate(_Reason, _State) -> ok.
136
137 -spec init(HostType :: mongooseim:host_type()) -> {'ok', state()}.
138 init(HostType) ->
139
:-(
State = parse_options(HostType),
140
:-(
{ok, State}.
141
142 -spec supports_sasl_module(mongooseim:host_type(), cyrsasl:sasl_module()) -> boolean().
143
:-(
supports_sasl_module(_, cyrsasl_plain) -> true;
144
:-(
supports_sasl_module(_, cyrsasl_external) -> true;
145
:-(
supports_sasl_module(_, _) -> false.
146
147 -spec authorize(mongoose_credentials:t()) -> {ok, mongoose_credentials:t()}
148 | {error, any()}.
149 authorize(Creds) ->
150
:-(
case mongoose_credentials:get(Creds, cert_file, false) of
151
:-(
true -> verify_user_exists(Creds);
152
:-(
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
:-(
case catch check_password_ldap(HostType, LUser, LServer, Password) of
162
:-(
{'EXIT', _} -> false;
163
:-(
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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
184
:-(
case find_user_dn(LUser, LServer, State) of
185
:-(
false -> {error, user_not_found};
186 DN ->
187
:-(
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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
196
:-(
UserStr = binary_to_list(LUser),
197
:-(
DN = "cn=" ++ UserStr ++ ", " ++ binary_to_list(State#state.base),
198
:-(
Attrs = [{"objectclass", ["inetOrgPerson"]},
199 {"cn", [UserStr]},
200 {"sn", [UserStr]},
201 {"userPassword", [binary_to_list(Password)]},
202 {"uid", [UserStr]}],
203
:-(
case eldap_pool:add(State#state.eldap_id, DN, Attrs) of
204
:-(
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
:-(
case catch get_registered_users_ldap(HostType, LServer) of
213
:-(
{'EXIT', _} -> [];
214
:-(
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
:-(
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
:-(
case catch does_user_exist_in_ldap(HostType, LUser, LServer) of
230
:-(
{'EXIT', Error} -> {error, Error};
231
:-(
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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
240
:-(
case find_user_dn(LUser, LServer, State) of
241
:-(
false -> {error, not_allowed};
242
:-(
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
:-(
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
:-(
User = mongoose_credentials:get(Creds, username),
257
:-(
case jid:nodeprep(User) of
258 error ->
259
:-(
error({nodeprep_error, User});
260 LUser ->
261
:-(
LServer = mongoose_credentials:lserver(Creds),
262
:-(
HostType = mongoose_credentials:host_type(Creds),
263
:-(
case does_user_exist(HostType, LUser, LServer) of
264
:-(
true -> {ok, mongoose_credentials:extend(Creds, [{auth_module, ?MODULE}])};
265
:-(
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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
275
:-(
case find_user_dn(LUser, LServer, State) of
276
:-(
false -> false;
277 DN ->
278
:-(
case eldap_pool:bind(State#state.bind_eldap_id, DN, Password) of
279
:-(
ok -> true;
280
:-(
_ -> 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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
288
:-(
UIDs = State#state.uids,
289
:-(
EldapID = State#state.eldap_id,
290
:-(
ResAttrs = result_attrs(State),
291
:-(
case eldap_filter:parse(State#state.sfilter) of
292 {ok, EldapFilter} ->
293
:-(
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
:-(
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
:-(
lists:flatmap(
310 fun(#eldap_entry{attributes = Attrs,
311 object_name = DN}) ->
312
:-(
case is_valid_dn(DN, LServer, Attrs, State) of
313
:-(
false -> [];
314 true ->
315
:-(
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
:-(
case eldap_utils:find_ldap_attrs(UIDs, Attributes) of
325
:-(
<<"">> -> [];
326 {User, UIDFormat} ->
327
:-(
case eldap_utils:get_user_part(User, UIDFormat) of
328 {ok, U} ->
329
:-(
[{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
:-(
{ok, State} = eldap_utils:get_state(HostType, ?MODULE),
339
:-(
case find_user_dn(LUser, LServer, State) of
340
:-(
false -> false;
341
:-(
_DN -> true
342 end.
343
344 handle_call(get_state, _From, State) ->
345
:-(
{reply, {ok, State}, State};
346 handle_call(stop, _From, State) ->
347
:-(
{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
:-(
ResAttrs = result_attrs(State),
356
:-(
case eldap_filter:parse(State#state.ufilter, [{<<"%u">>, LUser}]) of
357 {ok, Filter} ->
358
:-(
SearchOpts = find_user_opts(Filter, ResAttrs, State),
359
:-(
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
:-(
dn_filter(DN, LServer, Attrs, State);
365
:-(
_ -> false
366 end;
367
:-(
_ -> false
368 end.
369
370 find_user_opts(Filter, ResAttrs, State) ->
371
:-(
[{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
:-(
case check_local_filter(Attrs, State) of
382
:-(
false -> false;
383 true ->
384
:-(
case is_valid_dn(DN, LServer, Attrs, State) of
385
:-(
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
:-(
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
:-(
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
:-(
lists:foldl(fun ({UID}, Acc) -> [UID | Acc];
459
:-(
({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
:-(
Opts = mongoose_config:get_opt([{auth, HostType}, ldap]),
470
:-(
EldapID = {HostType, maps:get(pool_tag, Opts)},
471
:-(
BindEldapID = {HostType, maps:get(bind_pool_tag, Opts)},
472
:-(
Base = maps:get(base, Opts),
473
:-(
DerefAliases = eldap_utils:deref_aliases(maps:get(deref, Opts)),
474
:-(
RawUIDs = maps:get(uids, Opts),
475
:-(
UIDs = eldap_utils:uids_domain_subst(HostType, RawUIDs),
476
:-(
RawUserFilter = maps:get(filter, Opts),
477
:-(
UserFilter = eldap_utils:process_user_filter(UIDs, RawUserFilter),
478
:-(
SearchFilter = eldap_utils:get_search_filter(UserFilter),
479
:-(
{DNFilter, DNFilterAttrs} = maps:get(dn_filter, Opts),
480
:-(
LocalFilter = maps:get(local_filter, Opts),
481
:-(
#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