• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / emqx / 12810504881

16 Jan 2025 02:02PM UTC coverage: 82.34%. First build
12810504881

Pull #14556

github

web-flow
Merge 8ee1501fa into 63d3f11d8
Pull Request #14556: feat(authn): do not allow authentication if auth enabled but no hooks handled auth

29 of 40 new or added lines in 2 files covered. (72.5%)

57691 of 70064 relevant lines covered (82.34%)

15199.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.25
/apps/emqx_auth/src/emqx_authn/emqx_authn_chains.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2021-2025 EMQ Technologies Co., Ltd. All Rights Reserved.
3
%%
4
%% Licensed under the Apache License, Version 2.0 (the "License");
5
%% you may not use this file except in compliance with the License.
6
%% You may obtain a copy of the License at
7
%%
8
%%     http://www.apache.org/licenses/LICENSE-2.0
9
%%
10
%% Unless required by applicable law or agreed to in writing, software
11
%% distributed under the License is distributed on an "AS IS" BASIS,
12
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
%% See the License for the specific language governing permissions and
14
%% limitations under the License.
15
%%--------------------------------------------------------------------
16

17
%% @doc Authenticator management API module.
18
%% Authentication is a core functionality of MQTT,
19
%% the 'emqx' APP provides APIs for other APPs to implement
20
%% the authentication callbacks.
21
-module(emqx_authn_chains).
22

23
-behaviour(gen_server).
24

25
-include("emqx_authn_chains.hrl").
26
-include_lib("emqx/include/logger.hrl").
27
-include_lib("emqx/include/emqx_hooks.hrl").
28
-include_lib("stdlib/include/ms_transform.hrl").
29
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
30

31
-define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
32

33
-record(authenticator, {
34
    id :: binary(),
35
    provider :: module(),
36
    enable :: boolean(),
37
    state :: map()
38
}).
39

40
-record(chain, {
41
    name :: atom(),
42
    authenticators :: [#authenticator{}]
43
}).
44

45
%% The authentication entrypoint.
46
-export([
47
    authenticate/2
48
]).
49

50
%% Authenticator manager process start/stop
51
-export([
52
    start_link/0,
53
    stop/0,
54
    get_providers/0
55
]).
56

57
%% Authenticator management APIs
58
-export([
59
    register_provider/2,
60
    register_providers/1,
61
    deregister_provider/1,
62
    deregister_providers/1,
63
    delete_chain/1,
64
    lookup_chain/1,
65
    list_chains/0,
66
    list_chain_names/0,
67
    create_authenticator/2,
68
    delete_authenticator/2,
69
    update_authenticator/3,
70
    lookup_authenticator/2,
71
    list_authenticators/1,
72
    move_authenticator/3,
73
    reorder_authenticator/2
74
]).
75

76
%% APIs for observer built_in_database
77
-export([
78
    import_users/3,
79
    add_user/3,
80
    delete_user/3,
81
    update_user/4,
82
    lookup_user/3,
83
    list_users/3
84
]).
85

86
%% gen_server callbacks
87
-export([
88
    init/1,
89
    handle_call/3,
90
    handle_cast/2,
91
    handle_info/2,
92
    handle_continue/2,
93
    terminate/2,
94
    code_change/3
95
]).
96

97
%% utility functions
98
-export([authenticator_id/1, metrics_id/2]).
99

100
-export_type([
101
    authenticator_id/0,
102
    position/0,
103
    chain_name/0,
104
    authn_type/0
105
]).
106

107
-ifdef(TEST).
108
-compile(export_all).
109
-compile(nowarn_export_all).
110
-endif.
111

112
-define(CHAINS_TAB, emqx_authn_chains).
113

114
-define(TRACE_RESULT(Label, Result, Reason), begin
115
    ?TRACE_AUTHN(Label, #{
116
        result => (Result),
117
        reason => (Reason)
118
    }),
119
    Result
120
end).
121

122
-type chain_name() :: atom().
123
-type authenticator_id() :: binary().
124
-type position() :: front | rear | {before, authenticator_id()} | {'after', authenticator_id()}.
125
-type authn_type() :: atom() | {atom(), atom()}.
126
-type provider() :: module().
127

128
-type chain() :: #{
129
    name := chain_name(),
130
    authenticators := [authenticator()]
131
}.
132

133
-type authenticator() :: #{
134
    id := authenticator_id(),
135
    provider := provider(),
136
    enable := boolean(),
137
    state := map()
138
}.
139

140
-type config() :: emqx_authn_config:config().
141
-type state() :: #{atom() => term()}.
142
-type extra() :: #{
143
    is_superuser := boolean(),
144
    %% millisecond timestamp
145
    expire_at => pos_integer(),
146
    atom() => term()
147
}.
148
-type user_info() :: #{
149
    user_id := binary(),
150
    atom() => term()
151
}.
152

153
-type import_users_result() :: #{
154
    total := non_neg_integer(),
155
    success := non_neg_integer(),
156
    override := non_neg_integer(),
157
    skipped := non_neg_integer(),
158
    failed := non_neg_integer()
159
}.
160

161
-export_type([authenticator/0, config/0, state/0, extra/0, user_info/0]).
162

163
%%------------------------------------------------------------------------------
164
%% Authenticate
165
%%------------------------------------------------------------------------------
166

167
authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
168
    case get_authenticators(Listener, global_chain(Protocol)) of
1,028✔
169
        {ok, ChainName, Authenticators} ->
170
            case get_enabled(Authenticators) of
453✔
171
                [] ->
172
                    %% Empty chain means allowed anonymous access.
173
                    %% Further authentication hooks may still forbid the access.
174
                    %% We return
175
                    %% {
176
                    %%   ok, %% to tell the hooks that we want to set a new AuthResult acc
177
                    %%   ok %% the actual new AuthResult acc
178
                    %% }
NEW
179
                    ?TRACE_RESULT("authentication_result", {ok, ok}, empty_chain);
×
180
                NAuthenticators ->
181
                    Result = do_authenticate(ChainName, NAuthenticators, Credential),
453✔
182
                    ?TRACE_RESULT("authentication_result", Result, chain_result)
453✔
183
            end;
184
        none ->
185
            ?TRACE_RESULT("authentication_result", {ok, ok}, no_chain);
575✔
186
        no_table ->
187
            %% This basically means that authn app crashed or stopped or being restarted.
NEW
188
            ?TRACE_RESULT("authentication_result", {ok, {error, not_authorized}}, no_chain_table)
×
189
    end.
190

191
get_authenticators(Listener, Global) ->
192
    %% Under load, some clients may still execute this function
193
    %% while the authn app crashed or stopped and removed the table.
194
    %% So we handle this in restrictive manner (see authenticate/2).
195
    try ets:lookup(?CHAINS_TAB, Listener) of
1,028✔
196
        [#chain{name = Name, authenticators = Authenticators}] ->
197
            {ok, Name, Authenticators};
19✔
198
        _ ->
199
            try ets:lookup(?CHAINS_TAB, Global) of
1,009✔
200
                [#chain{name = Name, authenticators = Authenticators}] ->
201
                    {ok, Name, Authenticators};
434✔
202
                _ ->
203
                    none
575✔
204
            catch
205
                error:badarg ->
NEW
206
                    no_table
×
207
            end
208
    catch
209
        error:badarg ->
NEW
210
            no_table
×
211
    end.
212

213
get_enabled(Authenticators) ->
214
    [Authenticator || Authenticator <- Authenticators, Authenticator#authenticator.enable =:= true].
453✔
215

216
%%------------------------------------------------------------------------------
217
%% APIs
218
%%------------------------------------------------------------------------------
219

220
%% @doc Get all registered authentication providers.
221
-spec get_providers() -> #{authn_type() => module()}.
222
get_providers() ->
223
    call(get_providers).
2✔
224

225
%% @doc Get authenticator identifier from its config.
226
%% The authenticator config must contain a 'mechanism' key
227
%% and maybe a 'backend' key.
228
%% This function works with both parsed (atom keys) and raw (binary keys)
229
%% configurations.
230
-spec authenticator_id(config()) -> authenticator_id().
231
authenticator_id(Config) ->
232
    emqx_authn_config:authenticator_id(Config).
1,145✔
233

234
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
235
start_link() ->
236
    %% Create chains ETS table here so that it belongs to the supervisor
237
    %% and survives `emqx_authn_chains` crashes.
238
    ok = create_chain_table(),
224✔
239
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
224✔
240

241
-spec stop() -> ok.
242
stop() ->
243
    gen_server:stop(?MODULE).
×
244

245
%% @doc Register authentication providers.
246
%% A provider is a tuple of `AuthNType' the module which implements
247
%% the authenticator callbacks.
248
%% For example, ``[{{'password_based', redis}, emqx_authn_redis}]''
249
%% NOTE: Later registered provider may override earlier registered if they
250
%% happen to clash the same `AuthNType'.
251
-spec register_providers([{authn_type(), module()}]) -> ok | {error, term()}.
252
register_providers(Providers) ->
253
    call({register_providers, Providers}).
1,246✔
254

255
-spec register_provider(authn_type(), module()) -> ok.
256
register_provider(AuthNType, Provider) ->
257
    register_providers([{AuthNType, Provider}]).
1,230✔
258

259
-spec deregister_providers([authn_type()]) -> ok.
260
deregister_providers(AuthNTypes) when is_list(AuthNTypes) ->
261
    call({deregister_providers, AuthNTypes}).
1,061✔
262

263
-spec deregister_provider(authn_type()) -> ok.
264
deregister_provider(AuthNType) ->
265
    deregister_providers([AuthNType]).
1,057✔
266

267
-spec delete_chain(chain_name()) -> ok | {error, term()}.
268
delete_chain(Name) ->
269
    call({delete_chain, Name}).
32✔
270

271
-spec lookup_chain(chain_name()) -> {ok, chain()} | {error, term()}.
272
lookup_chain(Name) ->
273
    case ets:lookup(?CHAINS_TAB, Name) of
7✔
274
        [] ->
275
            {error, {not_found, {chain, Name}}};
2✔
276
        [Chain] ->
277
            {ok, serialize_chain(Chain)}
5✔
278
    end.
279

280
-spec list_chains() -> {ok, [chain()]}.
281
list_chains() ->
282
    Chains = ets:tab2list(?CHAINS_TAB),
20✔
283
    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
20✔
284

285
-spec list_chain_names() -> {ok, [atom()]}.
286
list_chain_names() ->
287
    Select = ets:fun2ms(fun(#chain{name = Name}) -> Name end),
5✔
288
    ChainNames = ets:select(?CHAINS_TAB, Select),
5✔
289
    {ok, ChainNames}.
5✔
290

291
-spec create_authenticator(chain_name(), config()) -> {ok, authenticator()} | {error, term()}.
292
create_authenticator(ChainName, Config) ->
293
    call({create_authenticator, ChainName, Config}).
364✔
294

295
-spec delete_authenticator(chain_name(), authenticator_id()) -> ok | {error, term()}.
296
delete_authenticator(ChainName, AuthenticatorID) ->
297
    call({delete_authenticator, ChainName, AuthenticatorID}).
262✔
298

299
-spec update_authenticator(chain_name(), authenticator_id(), config()) ->
300
    {ok, authenticator()} | {error, term()}.
301
update_authenticator(ChainName, AuthenticatorID, Config) ->
302
    call({update_authenticator, ChainName, AuthenticatorID, Config}).
69✔
303

304
-spec lookup_authenticator(chain_name(), authenticator_id()) ->
305
    {ok, authenticator()} | {error, term()}.
306
lookup_authenticator(ChainName, AuthenticatorID) ->
307
    case ets:lookup(?CHAINS_TAB, ChainName) of
157✔
308
        [] ->
309
            {error, {not_found, {chain, ChainName}}};
1✔
310
        [#chain{authenticators = Authenticators}] ->
311
            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
156✔
312
                false ->
313
                    {error, {not_found, {authenticator, AuthenticatorID}}};
2✔
314
                Authenticator ->
315
                    {ok, serialize_authenticator(Authenticator)}
154✔
316
            end
317
    end.
318

319
-spec list_authenticators(chain_name()) -> {ok, [authenticator()]} | {error, term()}.
320
list_authenticators(ChainName) ->
321
    case ets:lookup(?CHAINS_TAB, ChainName) of
580✔
322
        [] ->
323
            {error, {not_found, {chain, ChainName}}};
237✔
324
        [#chain{authenticators = Authenticators}] ->
325
            {ok, serialize_authenticators(Authenticators)}
343✔
326
    end.
327

328
-spec move_authenticator(chain_name(), authenticator_id(), position()) -> ok | {error, term()}.
329
move_authenticator(ChainName, AuthenticatorID, Position) ->
330
    call({move_authenticator, ChainName, AuthenticatorID, Position}).
10✔
331

332
-spec reorder_authenticator(chain_name(), [authenticator_id()]) -> ok.
333
reorder_authenticator(_ChainName, []) ->
334
    ok;
6✔
335
reorder_authenticator(ChainName, AuthenticatorIDs) ->
336
    call({reorder_authenticator, ChainName, AuthenticatorIDs}).
24✔
337

338
-spec import_users(
339
    chain_name(),
340
    authenticator_id(),
341
    {plain | hash, prepared_user_list | binary(), binary()}
342
) ->
343
    {ok, import_users_result()} | {error, term()}.
344
import_users(ChainName, AuthenticatorID, Filename) ->
345
    call({import_users, ChainName, AuthenticatorID, Filename}).
9✔
346

347
-spec add_user(chain_name(), authenticator_id(), user_info()) ->
348
    {ok, user_info()} | {error, term()}.
349
add_user(ChainName, AuthenticatorID, UserInfo) ->
350
    call({add_user, ChainName, AuthenticatorID, UserInfo}).
15✔
351

352
-spec delete_user(chain_name(), authenticator_id(), binary()) -> ok | {error, term()}.
353
delete_user(ChainName, AuthenticatorID, UserID) ->
354
    call({delete_user, ChainName, AuthenticatorID, UserID}).
7✔
355

356
-spec update_user(chain_name(), authenticator_id(), binary(), map()) ->
357
    {ok, user_info()} | {error, term()}.
358
update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) ->
359
    call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}).
4✔
360

361
-spec lookup_user(chain_name(), authenticator_id(), binary()) ->
362
    {ok, user_info()} | {error, term()}.
363
lookup_user(ChainName, AuthenticatorID, UserID) ->
364
    call({lookup_user, ChainName, AuthenticatorID, UserID}).
7✔
365

366
-spec list_users(chain_name(), authenticator_id(), map()) -> {ok, [user_info()]} | {error, term()}.
367
list_users(ChainName, AuthenticatorID, FuzzyParams) ->
368
    call({list_users, ChainName, AuthenticatorID, FuzzyParams}).
11✔
369

370
%%--------------------------------------------------------------------
371
%% gen_server callbacks
372
%%--------------------------------------------------------------------
373

374
init(_Opts) ->
375
    process_flag(trap_exit, true),
224✔
376
    Module = emqx_authn_config,
224✔
377
    ok = emqx_config_handler:add_handler([?CONF_ROOT], Module),
224✔
378
    ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], Module),
224✔
379
    {ok, #{providers => #{}, init_done => false}, {continue, initialize_authentication}}.
224✔
380

381
handle_call(get_providers, _From, #{providers := Providers} = State) ->
382
    reply(Providers, State);
2✔
383
handle_call(
384
    {register_providers, Providers},
385
    _From,
386
    #{providers := Reg0} = State
387
) ->
388
    case lists:filter(fun({T, _}) -> maps:is_key(T, Reg0) end, Providers) of
1,246✔
389
        [] ->
390
            Reg = lists:foldl(
1,246✔
391
                fun({AuthNType, Module}, Pin) ->
392
                    Pin#{AuthNType => Module}
1,252✔
393
                end,
394
                Reg0,
395
                Providers
396
            ),
397
            reply(ok, State#{providers := Reg}, initialize_authentication);
1,246✔
398
        Clashes ->
399
            reply({error, {authentication_type_clash, Clashes}}, State)
×
400
    end;
401
handle_call({deregister_providers, AuthNTypes}, _From, #{providers := Providers} = State) ->
402
    reply(ok, State#{providers := maps:without(AuthNTypes, Providers)});
1,061✔
403
%% Do not handle anything else before initialization is done.
404
%% TODO convert gen_server to gen_statem
405
handle_call(_, _From, #{init_done := false, providers := Providers} = State) ->
406
    ProviderTypes = maps:keys(Providers),
×
407
    Chains = chain_configs(),
×
408
    ?SLOG(error, #{
×
409
        msg => "authentication_not_initialized",
410
        configured_provider_types => configured_provider_types(Chains),
411
        registered_provider_types => ProviderTypes
412
    }),
×
413
    reply({error, not_initialized}, State);
×
414
handle_call({delete_chain, ChainName}, _From, State) ->
415
    UpdateFun = fun(Chain) ->
32✔
416
        {_MatchedIDs, NewChain} = do_delete_authenticators(fun(_) -> true end, Chain),
27✔
417
        {ok, ok, NewChain}
27✔
418
    end,
419
    Reply = with_chain(ChainName, UpdateFun),
32✔
420
    reply(Reply, State);
32✔
421
handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) ->
422
    UpdateFun = fun(Chain) ->
364✔
423
        handle_create_authenticator(Chain, Config, Providers)
364✔
424
    end,
425
    Reply = with_new_chain(ChainName, UpdateFun),
364✔
426
    reply(Reply, State);
364✔
427
handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) ->
428
    UpdateFun = fun(Chain) ->
262✔
429
        handle_delete_authenticator(Chain, AuthenticatorID)
262✔
430
    end,
431
    Reply = with_chain(ChainName, UpdateFun),
262✔
432
    reply(Reply, State);
262✔
433
handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) ->
434
    UpdateFun = fun(Chain) ->
69✔
435
        handle_update_authenticator(Chain, AuthenticatorID, Config)
67✔
436
    end,
437
    Reply = with_chain(ChainName, UpdateFun),
69✔
438
    reply(Reply, State);
69✔
439
handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) ->
440
    UpdateFun = fun(Chain) ->
10✔
441
        handle_move_authenticator(Chain, AuthenticatorID, Position)
10✔
442
    end,
443
    Reply = with_chain(ChainName, UpdateFun),
10✔
444
    reply(Reply, State);
10✔
445
handle_call({reorder_authenticator, ChainName, AuthenticatorIDs}, _From, State) ->
446
    UpdateFun = fun(Chain) ->
24✔
447
        handle_reorder_authenticator(Chain, AuthenticatorIDs)
24✔
448
    end,
449
    Reply = with_chain(ChainName, UpdateFun),
24✔
450
    reply(Reply, State);
24✔
451
handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) ->
452
    Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]),
9✔
453
    reply(Reply, State);
9✔
454
handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) ->
455
    Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]),
15✔
456
    reply(Reply, State);
15✔
457
handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
458
    Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]),
7✔
459
    reply(Reply, State);
7✔
460
handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
461
    Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]),
4✔
462
    reply(Reply, State);
4✔
463
handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
464
    Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]),
7✔
465
    reply(Reply, State);
7✔
466
handle_call({list_users, ChainName, AuthenticatorID, FuzzyParams}, _From, State) ->
467
    Reply = call_authenticator(ChainName, AuthenticatorID, list_users, [FuzzyParams]),
11✔
468
    reply(Reply, State);
11✔
469
handle_call(Req, _From, State) ->
470
    ?SLOG(error, #{msg => "unexpected_call", call => Req}),
×
471
    {reply, ignored, State}.
×
472

473
handle_continue(initialize_authentication, #{init_done := true} = State) ->
474
    {noreply, State};
1,241✔
475
handle_continue(initialize_authentication, #{providers := Providers} = State) ->
476
    InitDone = initialize_authentication(Providers),
229✔
477
    {noreply, State#{init_done := InitDone}}.
229✔
478

479
handle_cast(Req, State) ->
480
    ?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
×
481
    {noreply, State}.
×
482

483
handle_info(Info, State) ->
484
    ?SLOG(error, #{msg => "unexpected_info", info => Info}),
×
485
    {noreply, State}.
×
486

487
terminate(Reason, _State) ->
488
    case Reason of
180✔
489
        {shutdown, _} ->
490
            ok;
×
491
        Reason when Reason == normal; Reason == shutdown ->
492
            ok;
180✔
493
        Other ->
494
            ?SLOG(error, #{
×
495
                msg => "emqx_authentication_terminating",
496
                reason => Other
497
            })
×
498
    end,
499
    ok = unhook(),
180✔
500
    emqx_config_handler:remove_handler([?CONF_ROOT]),
180✔
501
    emqx_config_handler:remove_handler([listeners, '?', '?', ?CONF_ROOT]),
180✔
502
    ok.
180✔
503

504
code_change(_OldVsn, State, _Extra) ->
505
    {ok, State}.
×
506

507
%%------------------------------------------------------------------------------
508
%% Private functions
509
%%------------------------------------------------------------------------------
510

511
initialize_authentication(Providers) ->
512
    ProviderTypes = maps:keys(Providers),
229✔
513
    Chains = chain_configs(),
229✔
514
    HasProviders = has_providers_for_configs(Chains, ProviderTypes),
229✔
515
    do_initialize_authentication(Providers, Chains, HasProviders).
229✔
516

517
do_initialize_authentication(_Providers, _Chains, _HasProviders = false) ->
518
    false;
5✔
519
do_initialize_authentication(Providers, Chains, _HasProviders = true) ->
520
    ok = lists:foreach(
224✔
521
        fun({ChainName, ChainConfigs}) ->
522
            initialize_chain_authentication(Providers, ChainName, ChainConfigs)
1,122✔
523
        end,
524
        Chains
525
    ),
526
    ok = hook(),
224✔
527
    true.
224✔
528

529
initialize_chain_authentication(_Providers, _ChainName, []) ->
530
    ok;
1,119✔
531
initialize_chain_authentication(Providers, ChainName, AuthenticatorsConfig) ->
532
    lists:foreach(
3✔
533
        fun(AuthenticatorConfig) ->
534
            CreateResult = with_new_chain(ChainName, fun(Chain) ->
5✔
535
                handle_create_authenticator(Chain, AuthenticatorConfig, Providers)
5✔
536
            end),
537
            case CreateResult of
5✔
538
                {ok, _} ->
539
                    ok;
5✔
540
                {error, Reason} ->
541
                    ?SLOG(error, #{
×
542
                        msg => "failed_to_create_authenticator",
543
                        authenticator => authenticator_id(AuthenticatorConfig),
544
                        reason => Reason
545
                    })
×
546
            end
547
        end,
548
        to_list(AuthenticatorsConfig)
549
    ).
550

551
has_providers_for_configs(Chains, ProviderTypes) ->
552
    (configured_provider_types(Chains) -- ProviderTypes) =:= [].
229✔
553

554
configured_provider_types(Chains) ->
555
    {_, ChainConfs} = lists:unzip(Chains),
229✔
556
    ProviderTypes = lists:flatmap(
229✔
557
        fun provider_types_for_chain/1,
558
        ChainConfs
559
    ),
560
    lists:usort(ProviderTypes).
229✔
561

562
provider_types_for_chain(AuthConfig) ->
563
    Configs = to_list(AuthConfig),
1,147✔
564
    lists:map(
1,147✔
565
        fun(Config) ->
566
            provider_type_for_config(Config)
14✔
567
        end,
568
        Configs
569
    ).
570

571
provider_type_for_config(#{mechanism := Mechanism, backend := Backend}) ->
572
    {Mechanism, Backend};
14✔
573
provider_type_for_config(#{mechanism := Mechanism}) ->
574
    Mechanism.
×
575

576
handle_update_authenticator(Chain, AuthenticatorID, Config) ->
577
    #chain{authenticators = Authenticators} = Chain,
67✔
578
    case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
67✔
579
        false ->
580
            {error, {not_found, {authenticator, AuthenticatorID}}};
×
581
        #authenticator{provider = Provider, state = ST} = Authenticator ->
582
            case AuthenticatorID =:= authenticator_id(Config) of
67✔
583
                true ->
584
                    NConfig = insert_user_group(Chain, Config),
67✔
585
                    case Provider:update(NConfig, ST) of
67✔
586
                        {ok, NewST} ->
587
                            NewAuthenticator = Authenticator#authenticator{
67✔
588
                                state = NewST,
589
                                enable = maps:get(enable, NConfig)
590
                            },
591
                            NewAuthenticators = replace_authenticator(
67✔
592
                                AuthenticatorID,
593
                                NewAuthenticator,
594
                                Authenticators
595
                            ),
596
                            NewChain = Chain#chain{authenticators = NewAuthenticators},
67✔
597
                            Result = {ok, serialize_authenticator(NewAuthenticator)},
67✔
598
                            {ok, Result, NewChain};
67✔
599
                        {error, Reason} ->
600
                            {error, Reason}
×
601
                    end;
602
                false ->
603
                    {error, change_of_authentication_type_is_not_allowed}
×
604
            end
605
    end.
606

607
handle_delete_authenticator(Chain, AuthenticatorID) ->
608
    MatchFun = fun(#authenticator{id = ID}) ->
262✔
609
        ID =:= AuthenticatorID
275✔
610
    end,
611
    case do_delete_authenticators(MatchFun, Chain) of
262✔
612
        {[], NewChain} ->
613
            %% Idempotence intended
614
            {ok, ok, NewChain};
2✔
615
        {[AuthenticatorID], NewChain} ->
616
            {ok, ok, NewChain}
260✔
617
    end.
618

619
handle_move_authenticator(Chain, AuthenticatorID, Position) ->
620
    #chain{authenticators = Authenticators} = Chain,
10✔
621
    case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
10✔
622
        {ok, NAuthenticators} ->
623
            NewChain = Chain#chain{authenticators = NAuthenticators},
10✔
624
            {ok, ok, NewChain};
10✔
625
        {error, Reason} ->
626
            {error, Reason}
×
627
    end.
628

629
handle_reorder_authenticator(Chain, AuthenticatorIDs) ->
630
    #chain{authenticators = Authenticators} = Chain,
24✔
631
    NAuthenticators =
24✔
632
        lists:filtermap(
633
            fun(ID) ->
634
                case lists:keyfind(ID, #authenticator.id, Authenticators) of
69✔
635
                    false ->
636
                        ?SLOG(error, #{msg => "authenticator_not_found", id => ID}),
×
637
                        false;
×
638
                    Authenticator ->
639
                        {true, Authenticator}
69✔
640
                end
641
            end,
642
            AuthenticatorIDs
643
        ),
644
    NewChain = Chain#chain{authenticators = NAuthenticators},
24✔
645
    {ok, ok, NewChain}.
24✔
646

647
handle_create_authenticator(Chain, Config, Providers) ->
648
    #chain{name = Name, authenticators = Authenticators} = Chain,
369✔
649
    AuthenticatorID = authenticator_id(Config),
369✔
650
    case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of
369✔
651
        true ->
652
            {error, {already_exists, {authenticator, AuthenticatorID}}};
1✔
653
        false ->
654
            NConfig = insert_user_group(Chain, Config),
368✔
655
            case do_create_authenticator(AuthenticatorID, NConfig, Providers) of
368✔
656
                {ok, Authenticator} ->
657
                    NAuthenticators =
349✔
658
                        Authenticators ++
659
                            [Authenticator#authenticator{enable = maps:get(enable, Config)}],
660
                    ok = emqx_metrics_worker:create_metrics(
349✔
661
                        authn_metrics,
662
                        metrics_id(Name, AuthenticatorID),
663
                        [total, success, failed, nomatch],
664
                        [total]
665
                    ),
666
                    NewChain = Chain#chain{authenticators = NAuthenticators},
349✔
667
                    Result = {ok, serialize_authenticator(Authenticator)},
349✔
668
                    {ok, Result, NewChain};
349✔
669
                {error, Reason} ->
670
                    {error, Reason}
6✔
671
            end
672
    end.
673

674
do_authenticate(_ChainName, [], _) ->
675
    {ok, {error, not_authorized}};
156✔
676
do_authenticate(
677
    ChainName, [#authenticator{id = ID} = Authenticator | More], Credential
678
) ->
679
    MetricsID = metrics_id(ChainName, ID),
453✔
680
    emqx_metrics_worker:inc(authn_metrics, MetricsID, total),
453✔
681
    try authenticate_with_provider(Authenticator, Credential) of
453✔
682
        ignore ->
683
            ok = emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
156✔
684
            do_authenticate(ChainName, More, Credential);
156✔
685
        Result ->
686
            %% {ok, Extra}
687
            %% {ok, Extra, AuthData}
688
            %% {continue, AuthCache}
689
            %% {continue, AuthData, AuthCache}
690
            %% {error, Reason}
691
            case Result of
297✔
692
                {ok, _} ->
693
                    emqx_metrics_worker:inc(authn_metrics, MetricsID, success);
214✔
694
                {error, _} ->
695
                    emqx_metrics_worker:inc(authn_metrics, MetricsID, failed);
66✔
696
                _ ->
697
                    ok
17✔
698
            end,
699
            {stop, Result}
297✔
700
    catch
701
        Class:Reason:Stacktrace ->
702
            ?TRACE_AUTHN(
×
703
                warning,
×
704
                "authenticator_error",
705
                maybe_add_stacktrace(
×
706
                    Class,
707
                    #{
708
                        exception => Class,
709
                        reason => Reason,
710
                        authenticator => ID
711
                    },
712
                    Stacktrace
713
                )
×
714
            ),
715
            emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
×
716
            do_authenticate(ChainName, More, Credential)
×
717
    end.
718

719
maybe_add_stacktrace('throw', Data, _Stacktrace) ->
720
    Data;
×
721
maybe_add_stacktrace(_, Data, Stacktrace) ->
722
    Data#{stacktrace => Stacktrace}.
×
723

724
authenticate_with_provider(#authenticator{id = ID, provider = Provider, state = State}, Credential) ->
725
    AuthnResult = Provider:authenticate(Credential, State),
453✔
726
    ?TRACE_AUTHN("authenticator_result", #{
453✔
727
        authenticator => ID,
728
        result => AuthnResult
729
    }),
453✔
730
    AuthnResult.
453✔
731

732
reply(Reply, State) ->
733
    {reply, Reply, State}.
1,877✔
734

735
reply(Reply, State, Continue) ->
736
    {reply, Reply, State, {continue, Continue}}.
1,246✔
737

738
save_chain(#chain{
739
    name = Name,
740
    authenticators = []
741
}) ->
742
    ets:delete(?CHAINS_TAB, Name);
278✔
743
save_chain(#chain{} = Chain) ->
744
    ets:insert(?CHAINS_TAB, Chain).
461✔
745

746
create_chain_table() ->
747
    try
224✔
748
        _ = ets:new(?CHAINS_TAB, [
224✔
749
            named_table,
750
            set,
751
            public,
752
            {keypos, #chain.name},
753
            {read_concurrency, true}
754
        ]),
755
        ok
223✔
756
    catch
757
        error:badarg -> ok
1✔
758
    end.
759

760
global_chain(mqtt) ->
761
    'mqtt:global';
796✔
762
global_chain('mqtt-sn') ->
763
    'mqtt-sn:global';
72✔
764
global_chain(coap) ->
765
    'coap:global';
19✔
766
global_chain(lwm2m) ->
767
    'lwm2m:global';
41✔
768
global_chain(stomp) ->
769
    'stomp:global';
22✔
770
global_chain(_) ->
771
    'unknown:global'.
78✔
772

773
hook() ->
774
    ok = emqx_hooks:put('client.authenticate', {?MODULE, authenticate, []}, ?HP_AUTHN).
224✔
775

776
unhook() ->
777
    ok = emqx_hooks:del('client.authenticate', {?MODULE, authenticate, []}).
180✔
778

779
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
780
    Type = authn_type(Config),
368✔
781
    case maps:get(Type, Providers, undefined) of
368✔
782
        undefined ->
783
            {error, {no_available_provider_for, Type}};
1✔
784
        Provider ->
785
            case Provider:create(AuthenticatorID, Config) of
367✔
786
                {ok, State} ->
787
                    Authenticator = #authenticator{
349✔
788
                        id = AuthenticatorID,
789
                        provider = Provider,
790
                        enable = Enable,
791
                        state = State
792
                    },
793
                    {ok, Authenticator};
349✔
794
                {error, Reason} ->
795
                    {error, Reason}
5✔
796
            end
797
    end.
798

799
do_delete_authenticators(MatchFun, #chain{name = Name, authenticators = Authenticators} = Chain) ->
800
    {Matching, Others} = lists:partition(MatchFun, Authenticators),
289✔
801

802
    MatchingIDs = lists:map(
289✔
803
        fun(#authenticator{id = ID}) -> ID end,
288✔
804
        Matching
805
    ),
806

807
    ok = lists:foreach(
289✔
808
        fun(#authenticator{id = ID} = Authenticator) ->
809
            do_destroy_authenticator(Authenticator),
288✔
810
            emqx_metrics_worker:clear_metrics(authn_metrics, metrics_id(Name, ID))
288✔
811
        end,
812
        Matching
813
    ),
814
    {MatchingIDs, Chain#chain{authenticators = Others}}.
289✔
815

816
do_destroy_authenticator(#authenticator{provider = Provider, state = State}) ->
817
    _ = Provider:destroy(State),
288✔
818
    ok.
288✔
819

820
replace_authenticator(ID, Authenticator, Authenticators) ->
821
    lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator).
67✔
822

823
do_move_authenticator(ID, Authenticators, Position) ->
824
    case lists:keytake(ID, #authenticator.id, Authenticators) of
10✔
825
        false ->
826
            {error, {not_found, {authenticator, ID}}};
×
827
        {value, Authenticator, NAuthenticators} ->
828
            case Position of
10✔
829
                ?CMD_MOVE_FRONT ->
830
                    {ok, [Authenticator | NAuthenticators]};
4✔
831
                ?CMD_MOVE_REAR ->
832
                    {ok, NAuthenticators ++ [Authenticator]};
2✔
833
                ?CMD_MOVE_BEFORE(RelatedID) ->
834
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_BEFORE(RelatedID), []);
2✔
835
                ?CMD_MOVE_AFTER(RelatedID) ->
836
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_AFTER(RelatedID), [])
2✔
837
            end
838
    end.
839

840
insert(_, [], {_, RelatedID}, _) ->
841
    {error, {not_found, {authenticator, RelatedID}}};
×
842
insert(
843
    Authenticator,
844
    [#authenticator{id = RelatedID} = Related | Rest],
845
    {Relative, RelatedID},
846
    Acc
847
) ->
848
    case Relative of
4✔
849
        before ->
850
            {ok, lists:reverse(Acc) ++ [Authenticator, Related | Rest]};
2✔
851
        'after' ->
852
            {ok, lists:reverse(Acc) ++ [Related, Authenticator | Rest]}
2✔
853
    end;
854
insert(Authenticator, [Authenticator0 | More], {Relative, RelatedID}, Acc) ->
855
    insert(Authenticator, More, {Relative, RelatedID}, [Authenticator0 | Acc]).
1✔
856

857
with_new_chain(ChainName, Fun) ->
858
    case ets:lookup(?CHAINS_TAB, ChainName) of
369✔
859
        [] ->
860
            Chain = #chain{name = ChainName, authenticators = []},
320✔
861
            do_with_chain(Fun, Chain);
320✔
862
        [Chain] ->
863
            do_with_chain(Fun, Chain)
49✔
864
    end.
865

866
with_chain(ChainName, Fun) ->
867
    case ets:lookup(?CHAINS_TAB, ChainName) of
450✔
868
        [] ->
869
            {error, {not_found, {chain, ChainName}}};
9✔
870
        [Chain] ->
871
            do_with_chain(Fun, Chain)
441✔
872
    end.
873

874
do_with_chain(Fun, Chain) ->
875
    try
810✔
876
        case Fun(Chain) of
810✔
877
            {ok, Result} ->
878
                Result;
51✔
879
            {ok, Result, NewChain} ->
880
                save_chain(NewChain),
739✔
881
                Result;
739✔
882
            {error, _} = Error ->
883
                Error
7✔
884
        end
885
    catch
886
        Class:Reason:Stk ->
887
            {error, {exception, {Class, Reason, Stk}}}
13✔
888
    end.
889

890
call_authenticator(ChainName, AuthenticatorID, Func, Args) ->
891
    Fun =
53✔
892
        fun(#chain{authenticators = Authenticators}) ->
893
            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
51✔
894
                false ->
895
                    {error, {not_found, {authenticator, AuthenticatorID}}};
×
896
                #authenticator{provider = Provider, state = State} ->
897
                    case erlang:function_exported(Provider, Func, length(Args) + 1) of
51✔
898
                        true ->
899
                            {ok, erlang:apply(Provider, Func, Args ++ [State])};
51✔
900
                        false ->
901
                            {error, unsupported_operation}
×
902
                    end
903
            end
904
        end,
905
    with_chain(ChainName, Fun).
53✔
906

907
serialize_chain(#chain{
908
    name = Name,
909
    authenticators = Authenticators
910
}) ->
911
    #{
15✔
912
        name => Name,
913
        authenticators => serialize_authenticators(Authenticators)
914
    }.
915

916
serialize_authenticators(Authenticators) ->
917
    [serialize_authenticator(Authenticator) || Authenticator <- Authenticators].
358✔
918

919
serialize_authenticator(#authenticator{
920
    id = ID,
921
    provider = Provider,
922
    enable = Enable,
923
    state = State
924
}) ->
925
    #{
946✔
926
        id => ID,
927
        provider => Provider,
928
        enable => Enable,
929
        state => State
930
    }.
931

932
authn_type(#{mechanism := Mechanism, backend := Backend}) ->
933
    {Mechanism, Backend};
339✔
934
authn_type(#{mechanism := Mechanism}) ->
935
    Mechanism.
29✔
936

937
insert_user_group(
938
    Chain,
939
    Config = #{
940
        mechanism := password_based,
941
        backend := built_in_database
942
    }
943
) ->
944
    Config#{user_group => Chain#chain.name};
74✔
945
insert_user_group(_Chain, Config) ->
946
    Config.
361✔
947

948
metrics_id(ChainName, AuthenticatorId) ->
949
    iolist_to_binary([atom_to_binary(ChainName), <<"-">>, AuthenticatorId]).
1,237✔
950

951
chain_configs() ->
952
    [global_chain_config() | listener_chain_configs()].
229✔
953

954
global_chain_config() ->
955
    {?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
229✔
956

957
listener_chain_configs() ->
958
    lists:map(
229✔
959
        fun({ListenerID, _}) ->
960
            {ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
918✔
961
        end,
962
        emqx_listeners:list()
963
    ).
964

965
auth_config_path(ListenerID) ->
966
    Names = [
918✔
967
        binary_to_existing_atom(N, utf8)
1,836✔
968
     || N <- binary:split(atom_to_binary(ListenerID), <<":">>)
918✔
969
    ],
970
    [listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
918✔
971

972
to_list(undefined) -> [];
×
973
to_list(M) when M =:= #{} -> [];
×
974
to_list(M) when is_map(M) -> [M];
×
975
to_list(L) when is_list(L) -> L.
1,150✔
976

977
call(Call) -> gen_server:call(?MODULE, Call, infinity).
3,123✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc