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

emqx / emqx / 12806424729

16 Jan 2025 09:59AM UTC coverage: 81.972%. First build
12806424729

Pull #14479

github

web-flow
Merge 648164338 into db3af612f
Pull Request #14479: feat(e2e): authn/authz backend trace

39 of 62 new or added lines in 3 files covered. (62.9%)

56958 of 69485 relevant lines covered (81.97%)

15113.8 hits per line

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

85.93
/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("emqx/include/emqx_external_trace.hrl").
29
-include_lib("stdlib/include/ms_transform.hrl").
30
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
31

32
-define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
33

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

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

46
%% The authentication entrypoint.
47
-export([
48
    authenticate/2,
49
    authenticate_deny/2
50
]).
51

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

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

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

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

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

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

109
-ifdef(TEST).
110
-compile(export_all).
111
-compile(nowarn_export_all).
112
-endif.
113

114
-define(CHAINS_TAB, emqx_authn_chains).
115

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

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

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

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

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

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

163
-export_type([authenticator/0, config/0, state/0, extra/0, user_info/0]).
164

165
%%------------------------------------------------------------------------------
166
%% Authenticate
167
%%------------------------------------------------------------------------------
168

169
authenticate(#{listener := Listener, protocol := Protocol} = Credential, AuthResult) ->
170
    case get_authenticators(Listener, global_chain(Protocol)) of
418✔
171
        {ok, ChainName, Authenticators} ->
172
            case get_enabled(Authenticators) of
418✔
173
                [] ->
174
                    ?TRACE_RESULT("authentication_result", AuthResult, empty_chain);
×
175
                NAuthenticators ->
176
                    Result = do_authenticate(ChainName, NAuthenticators, Credential),
418✔
177
                    ?TRACE_RESULT("authentication_result", Result, chain_result)
418✔
178
            end;
179
        none ->
180
            ?TRACE_RESULT("authentication_result", AuthResult, no_chain)
×
181
    end.
182

183
authenticate_deny(_Credential, _AuthResult) ->
184
    ?TRACE_RESULT("authentication_result", {ok, {error, not_authorized}}, not_initialized).
1✔
185

186
get_authenticators(Listener, Global) ->
187
    case ets:lookup(?CHAINS_TAB, Listener) of
418✔
188
        [#chain{name = Name, authenticators = Authenticators}] ->
189
            {ok, Name, Authenticators};
19✔
190
        _ ->
191
            case ets:lookup(?CHAINS_TAB, Global) of
399✔
192
                [#chain{name = Name, authenticators = Authenticators}] ->
193
                    {ok, Name, Authenticators};
399✔
194
                _ ->
195
                    none
×
196
            end
197
    end.
198

199
get_enabled(Authenticators) ->
200
    [Authenticator || Authenticator <- Authenticators, Authenticator#authenticator.enable =:= true].
418✔
201

202
%%------------------------------------------------------------------------------
203
%% APIs
204
%%------------------------------------------------------------------------------
205

206
%% @doc Get all registered authentication providers.
207
-spec get_providers() -> #{authn_type() => module()}.
208
get_providers() ->
209
    call(get_providers).
2✔
210

211
%% @doc Get authenticator identifier from its config.
212
%% The authenticator config must contain a 'mechanism' key
213
%% and maybe a 'backend' key.
214
%% This function works with both parsed (atom keys) and raw (binary keys)
215
%% configurations.
216
-spec authenticator_id(config()) -> authenticator_id().
217
authenticator_id(Config) ->
218
    emqx_authn_config:authenticator_id(Config).
1,134✔
219

220
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
221
start_link() ->
222
    %% Create chains ETS table here so that it belongs to the supervisor
223
    %% and survives `emqx_authn_chains` crashes.
224
    ok = create_chain_table(),
223✔
225
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
223✔
226

227
-spec stop() -> ok.
228
stop() ->
229
    gen_server:stop(?MODULE).
×
230

231
%% @doc Register authentication providers.
232
%% A provider is a tuple of `AuthNType' the module which implements
233
%% the authenticator callbacks.
234
%% For example, ``[{{'password_based', redis}, emqx_authn_redis}]''
235
%% NOTE: Later registered provider may override earlier registered if they
236
%% happen to clash the same `AuthNType'.
237
-spec register_providers([{authn_type(), module()}]) -> ok | {error, term()}.
238
register_providers(Providers) ->
239
    call({register_providers, Providers}).
1,246✔
240

241
-spec register_provider(authn_type(), module()) -> ok.
242
register_provider(AuthNType, Provider) ->
243
    register_providers([{AuthNType, Provider}]).
1,230✔
244

245
-spec deregister_providers([authn_type()]) -> ok.
246
deregister_providers(AuthNTypes) when is_list(AuthNTypes) ->
247
    call({deregister_providers, AuthNTypes}).
1,051✔
248

249
-spec deregister_provider(authn_type()) -> ok.
250
deregister_provider(AuthNType) ->
251
    deregister_providers([AuthNType]).
1,047✔
252

253
-spec delete_chain(chain_name()) -> ok | {error, term()}.
254
delete_chain(Name) ->
255
    call({delete_chain, Name}).
32✔
256

257
-spec lookup_chain(chain_name()) -> {ok, chain()} | {error, term()}.
258
lookup_chain(Name) ->
259
    case ets:lookup(?CHAINS_TAB, Name) of
7✔
260
        [] ->
261
            {error, {not_found, {chain, Name}}};
2✔
262
        [Chain] ->
263
            {ok, serialize_chain(Chain)}
5✔
264
    end.
265

266
-spec list_chains() -> {ok, [chain()]}.
267
list_chains() ->
268
    Chains = ets:tab2list(?CHAINS_TAB),
20✔
269
    {ok, [serialize_chain(Chain) || Chain <- Chains]}.
20✔
270

271
-spec list_chain_names() -> {ok, [atom()]}.
272
list_chain_names() ->
273
    Select = ets:fun2ms(fun(#chain{name = Name}) -> Name end),
5✔
274
    ChainNames = ets:select(?CHAINS_TAB, Select),
5✔
275
    {ok, ChainNames}.
5✔
276

277
-spec create_authenticator(chain_name(), config()) -> {ok, authenticator()} | {error, term()}.
278
create_authenticator(ChainName, Config) ->
279
    call({create_authenticator, ChainName, Config}).
354✔
280

281
-spec delete_authenticator(chain_name(), authenticator_id()) -> ok | {error, term()}.
282
delete_authenticator(ChainName, AuthenticatorID) ->
283
    call({delete_authenticator, ChainName, AuthenticatorID}).
252✔
284

285
-spec update_authenticator(chain_name(), authenticator_id(), config()) ->
286
    {ok, authenticator()} | {error, term()}.
287
update_authenticator(ChainName, AuthenticatorID, Config) ->
288
    call({update_authenticator, ChainName, AuthenticatorID, Config}).
69✔
289

290
-spec lookup_authenticator(chain_name(), authenticator_id()) ->
291
    {ok, authenticator()} | {error, term()}.
292
lookup_authenticator(ChainName, AuthenticatorID) ->
293
    case ets:lookup(?CHAINS_TAB, ChainName) of
157✔
294
        [] ->
295
            {error, {not_found, {chain, ChainName}}};
1✔
296
        [#chain{authenticators = Authenticators}] ->
297
            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
156✔
298
                false ->
299
                    {error, {not_found, {authenticator, AuthenticatorID}}};
2✔
300
                Authenticator ->
301
                    {ok, serialize_authenticator(Authenticator)}
154✔
302
            end
303
    end.
304

305
-spec list_authenticators(chain_name()) -> {ok, [authenticator()]} | {error, term()}.
306
list_authenticators(ChainName) ->
307
    case ets:lookup(?CHAINS_TAB, ChainName) of
537✔
308
        [] ->
309
            {error, {not_found, {chain, ChainName}}};
205✔
310
        [#chain{authenticators = Authenticators}] ->
311
            {ok, serialize_authenticators(Authenticators)}
332✔
312
    end.
313

314
-spec move_authenticator(chain_name(), authenticator_id(), position()) -> ok | {error, term()}.
315
move_authenticator(ChainName, AuthenticatorID, Position) ->
316
    call({move_authenticator, ChainName, AuthenticatorID, Position}).
10✔
317

318
-spec reorder_authenticator(chain_name(), [authenticator_id()]) -> ok.
319
reorder_authenticator(_ChainName, []) ->
320
    ok;
6✔
321
reorder_authenticator(ChainName, AuthenticatorIDs) ->
322
    call({reorder_authenticator, ChainName, AuthenticatorIDs}).
24✔
323

324
-spec import_users(
325
    chain_name(),
326
    authenticator_id(),
327
    {plain | hash, prepared_user_list | binary(), binary()}
328
) ->
329
    {ok, import_users_result()} | {error, term()}.
330
import_users(ChainName, AuthenticatorID, Filename) ->
331
    call({import_users, ChainName, AuthenticatorID, Filename}).
9✔
332

333
-spec add_user(chain_name(), authenticator_id(), user_info()) ->
334
    {ok, user_info()} | {error, term()}.
335
add_user(ChainName, AuthenticatorID, UserInfo) ->
336
    call({add_user, ChainName, AuthenticatorID, UserInfo}).
15✔
337

338
-spec delete_user(chain_name(), authenticator_id(), binary()) -> ok | {error, term()}.
339
delete_user(ChainName, AuthenticatorID, UserID) ->
340
    call({delete_user, ChainName, AuthenticatorID, UserID}).
7✔
341

342
-spec update_user(chain_name(), authenticator_id(), binary(), map()) ->
343
    {ok, user_info()} | {error, term()}.
344
update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) ->
345
    call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}).
4✔
346

347
-spec lookup_user(chain_name(), authenticator_id(), binary()) ->
348
    {ok, user_info()} | {error, term()}.
349
lookup_user(ChainName, AuthenticatorID, UserID) ->
350
    call({lookup_user, ChainName, AuthenticatorID, UserID}).
7✔
351

352
-spec list_users(chain_name(), authenticator_id(), map()) -> {ok, [user_info()]} | {error, term()}.
353
list_users(ChainName, AuthenticatorID, FuzzyParams) ->
354
    call({list_users, ChainName, AuthenticatorID, FuzzyParams}).
11✔
355

356
%%--------------------------------------------------------------------
357
%% gen_server callbacks
358
%%--------------------------------------------------------------------
359

360
init(_Opts) ->
361
    process_flag(trap_exit, true),
223✔
362
    Module = emqx_authn_config,
223✔
363
    ok = emqx_config_handler:add_handler([?CONF_ROOT], Module),
223✔
364
    ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], Module),
223✔
365
    ok = hook_deny(),
223✔
366
    {ok, #{hooked => false, providers => #{}, init_done => false},
223✔
367
        {continue, initialize_authentication}}.
368

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

461
handle_continue(initialize_authentication, #{init_done := true} = State) ->
462
    {noreply, State};
1,241✔
463
handle_continue(initialize_authentication, #{providers := Providers} = State) ->
464
    InitDone = initialize_authentication(Providers),
228✔
465
    {noreply, maybe_hook(State#{init_done := InitDone})}.
228✔
466

467
handle_cast(Req, State) ->
468
    ?SLOG(error, #{msg => "unexpected_cast", cast => Req}),
×
469
    {noreply, State}.
×
470

471
handle_info(Info, State) ->
472
    ?SLOG(error, #{msg => "unexpected_info", info => Info}),
×
473
    {noreply, State}.
×
474

475
terminate(Reason, _State) ->
476
    case Reason of
179✔
477
        {shutdown, _} ->
478
            ok;
×
479
        Reason when Reason == normal; Reason == shutdown ->
480
            ok;
179✔
481
        Other ->
482
            ?SLOG(error, #{
×
483
                msg => "emqx_authentication_terminating",
484
                reason => Other
485
            })
×
486
    end,
487
    emqx_config_handler:remove_handler([?CONF_ROOT]),
179✔
488
    emqx_config_handler:remove_handler([listeners, '?', '?', ?CONF_ROOT]),
179✔
489
    ok.
179✔
490

491
code_change(_OldVsn, State, _Extra) ->
492
    {ok, State}.
×
493

494
%%------------------------------------------------------------------------------
495
%% Private functions
496
%%------------------------------------------------------------------------------
497

498
initialize_authentication(Providers) ->
499
    ProviderTypes = maps:keys(Providers),
228✔
500
    Chains = chain_configs(),
228✔
501
    HasProviders = has_providers_for_configs(Chains, ProviderTypes),
228✔
502
    do_initialize_authentication(Providers, Chains, HasProviders).
228✔
503

504
do_initialize_authentication(_Providers, _Chains, _HasProviders = false) ->
505
    false;
5✔
506
do_initialize_authentication(Providers, Chains, _HasProviders = true) ->
507
    ok = lists:foreach(
223✔
508
        fun({ChainName, ChainConfigs}) ->
509
            initialize_chain_authentication(Providers, ChainName, ChainConfigs)
1,117✔
510
        end,
511
        Chains
512
    ),
513
    ok = unhook_deny(),
223✔
514
    true.
223✔
515

516
initialize_chain_authentication(_Providers, _ChainName, []) ->
517
    ok;
1,114✔
518
initialize_chain_authentication(Providers, ChainName, AuthenticatorsConfig) ->
519
    lists:foreach(
3✔
520
        fun(AuthenticatorConfig) ->
521
            CreateResult = with_new_chain(ChainName, fun(Chain) ->
5✔
522
                handle_create_authenticator(Chain, AuthenticatorConfig, Providers)
5✔
523
            end),
524
            case CreateResult of
5✔
525
                {ok, _} ->
526
                    ok;
5✔
527
                {error, Reason} ->
528
                    ?SLOG(error, #{
×
529
                        msg => "failed_to_create_authenticator",
530
                        authenticator => authenticator_id(AuthenticatorConfig),
531
                        reason => Reason
532
                    })
×
533
            end
534
        end,
535
        to_list(AuthenticatorsConfig)
536
    ).
537

538
has_providers_for_configs(Chains, ProviderTypes) ->
539
    (configured_provider_types(Chains) -- ProviderTypes) =:= [].
228✔
540

541
configured_provider_types(Chains) ->
542
    {_, ChainConfs} = lists:unzip(Chains),
228✔
543
    ProviderTypes = lists:flatmap(
228✔
544
        fun provider_types_for_chain/1,
545
        ChainConfs
546
    ),
547
    lists:usort(ProviderTypes).
228✔
548

549
provider_types_for_chain(AuthConfig) ->
550
    Configs = to_list(AuthConfig),
1,142✔
551
    lists:map(
1,142✔
552
        fun(Config) ->
553
            provider_type_for_config(Config)
14✔
554
        end,
555
        Configs
556
    ).
557

558
provider_type_for_config(#{mechanism := Mechanism, backend := Backend}) ->
559
    {Mechanism, Backend};
14✔
560
provider_type_for_config(#{mechanism := Mechanism}) ->
561
    Mechanism.
×
562

563
handle_update_authenticator(Chain, AuthenticatorID, Config) ->
564
    #chain{authenticators = Authenticators} = Chain,
67✔
565
    case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
67✔
566
        false ->
567
            {error, {not_found, {authenticator, AuthenticatorID}}};
×
568
        #authenticator{provider = Provider, state = ST} = Authenticator ->
569
            case AuthenticatorID =:= authenticator_id(Config) of
67✔
570
                true ->
571
                    NConfig = insert_user_group(Chain, Config),
67✔
572
                    case Provider:update(NConfig, ST) of
67✔
573
                        {ok, NewST} ->
574
                            NewAuthenticator = Authenticator#authenticator{
67✔
575
                                state = NewST,
576
                                enable = maps:get(enable, NConfig)
577
                            },
578
                            NewAuthenticators = replace_authenticator(
67✔
579
                                AuthenticatorID,
580
                                NewAuthenticator,
581
                                Authenticators
582
                            ),
583
                            NewChain = Chain#chain{authenticators = NewAuthenticators},
67✔
584
                            Result = {ok, serialize_authenticator(NewAuthenticator)},
67✔
585
                            {ok, Result, NewChain};
67✔
586
                        {error, Reason} ->
587
                            {error, Reason}
×
588
                    end;
589
                false ->
590
                    {error, change_of_authentication_type_is_not_allowed}
×
591
            end
592
    end.
593

594
handle_delete_authenticator(Chain, AuthenticatorID) ->
595
    MatchFun = fun(#authenticator{id = ID}) ->
252✔
596
        ID =:= AuthenticatorID
265✔
597
    end,
598
    case do_delete_authenticators(MatchFun, Chain) of
252✔
599
        {[], NewChain} ->
600
            %% Idempotence intended
601
            {ok, ok, NewChain};
2✔
602
        {[AuthenticatorID], NewChain} ->
603
            {ok, ok, NewChain}
250✔
604
    end.
605

606
handle_move_authenticator(Chain, AuthenticatorID, Position) ->
607
    #chain{authenticators = Authenticators} = Chain,
10✔
608
    case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
10✔
609
        {ok, NAuthenticators} ->
610
            NewChain = Chain#chain{authenticators = NAuthenticators},
10✔
611
            {ok, ok, NewChain};
10✔
612
        {error, Reason} ->
613
            {error, Reason}
×
614
    end.
615

616
handle_reorder_authenticator(Chain, AuthenticatorIDs) ->
617
    #chain{authenticators = Authenticators} = Chain,
24✔
618
    NAuthenticators =
24✔
619
        lists:filtermap(
620
            fun(ID) ->
621
                case lists:keyfind(ID, #authenticator.id, Authenticators) of
69✔
622
                    false ->
623
                        ?SLOG(error, #{msg => "authenticator_not_found", id => ID}),
×
624
                        false;
×
625
                    Authenticator ->
626
                        {true, Authenticator}
69✔
627
                end
628
            end,
629
            AuthenticatorIDs
630
        ),
631
    NewChain = Chain#chain{authenticators = NAuthenticators},
24✔
632
    {ok, ok, NewChain}.
24✔
633

634
handle_create_authenticator(Chain, Config, Providers) ->
635
    #chain{name = Name, authenticators = Authenticators} = Chain,
359✔
636
    AuthenticatorID = authenticator_id(Config),
359✔
637
    case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of
359✔
638
        true ->
639
            {error, {already_exists, {authenticator, AuthenticatorID}}};
1✔
640
        false ->
641
            NConfig = insert_user_group(Chain, Config),
358✔
642
            case do_create_authenticator(AuthenticatorID, NConfig, Providers) of
358✔
643
                {ok, Authenticator} ->
644
                    NAuthenticators =
339✔
645
                        Authenticators ++
646
                            [Authenticator#authenticator{enable = maps:get(enable, Config)}],
647
                    ok = emqx_metrics_worker:create_metrics(
339✔
648
                        authn_metrics,
649
                        metrics_id(Name, AuthenticatorID),
650
                        [total, success, failed, nomatch],
651
                        [total]
652
                    ),
653
                    NewChain = Chain#chain{authenticators = NAuthenticators},
339✔
654
                    Result = {ok, serialize_authenticator(Authenticator)},
339✔
655
                    {ok, Result, NewChain};
339✔
656
                {error, Reason} ->
657
                    {error, Reason}
6✔
658
            end
659
    end.
660

661
do_authenticate(_ChainName, [], _) ->
662
    {ok, {error, not_authorized}};
142✔
663
do_authenticate(
664
    ChainName,
665
    [#authenticator{id = ID, provider = _Module} = Authenticator | More],
666
    Credential
667
) ->
668
    MetricsID = metrics_id(ChainName, ID),
418✔
669
    emqx_metrics_worker:inc(authn_metrics, MetricsID, total),
418✔
670

671
    Result = ?EXT_TRACE_WITH_PROCESS_FUN(
418✔
672
        client_authn_backend,
673
        #{
674
            'client.clientid' => maps:get(clientid, Credential, undefined),
675
            'client.username' => maps:get(username, Credential, undefined),
676
            'authn.authenticator' => ID,
677
            'authn.chain' => ChainName,
678
            'authn.module' => _Module
679
        },
680
        fun() ->
164✔
681
            try authenticate_with_provider(Authenticator, Credential) of
418✔
682
                ignore ->
683
                    ok = emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
142✔
684
                    ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ignore}),
142✔
685
                    ignore;
142✔
686
                Res ->
687
                    %% {ok, Extra}
688
                    %% {ok, Extra, AuthData} XXX: should inc metrics success for this clause?
689
                    %% {continue, AuthCache}
690
                    %% {continue, AuthData, AuthCache}
691
                    %% {error, Reason}
692
                    case Res of
276✔
693
                        {ok, _} ->
694
                            emqx_metrics_worker:inc(authn_metrics, MetricsID, success),
196✔
695
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ok});
196✔
696
                        {error, _} ->
697
                            emqx_metrics_worker:inc(authn_metrics, MetricsID, failed),
63✔
698
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => error}),
63✔
699
                            ?EXT_TRACE_SET_STATUS_ERROR();
63✔
700
                        _ ->
701
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ok}),
17✔
702
                            ok
17✔
703
                    end,
704
                    {stop, Res}
276✔
705
            catch
706
                Class:Reason:Stacktrace ->
NEW
707
                    ?TRACE_AUTHN(
×
NEW
708
                        warning,
×
709
                        "authenticator_error",
710
                        maybe_add_stacktrace(
711
                            Class,
712
                            #{
713
                                exception => Class,
714
                                reason => Reason,
715
                                authenticator => ID
716
                            },
717
                            Stacktrace
NEW
718
                        )
×
719
                    ),
NEW
720
                    emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
×
NEW
721
                    with_provider_failed
×
722
            end
723
        end
724
    ),
725
    case Result of
418✔
NEW
726
        with_provider_failed -> do_authenticate(ChainName, More, Credential);
×
727
        ignore -> do_authenticate(ChainName, More, Credential);
142✔
728
        {stop, _} -> Result
276✔
729
    end.
730

731
maybe_add_stacktrace('throw', Data, _Stacktrace) ->
732
    Data;
×
733
maybe_add_stacktrace(_, Data, Stacktrace) ->
734
    Data#{stacktrace => Stacktrace}.
×
735

736
authenticate_with_provider(#authenticator{id = ID, provider = Provider, state = State}, Credential) ->
737
    AuthnResult = Provider:authenticate(Credential, State),
418✔
738
    ?TRACE_AUTHN("authenticator_result", #{
418✔
739
        authenticator => ID,
740
        result => AuthnResult
741
    }),
418✔
742
    AuthnResult.
418✔
743

744
reply(Reply, State) ->
745
    {reply, Reply, State}.
1,847✔
746

747
reply(Reply, State, Continue) ->
748
    {reply, Reply, State, {continue, Continue}}.
1,246✔
749

750
save_chain(#chain{
751
    name = Name,
752
    authenticators = []
753
}) ->
754
    ets:delete(?CHAINS_TAB, Name);
268✔
755
save_chain(#chain{} = Chain) ->
756
    ets:insert(?CHAINS_TAB, Chain).
451✔
757

758
create_chain_table() ->
759
    try
223✔
760
        _ = ets:new(?CHAINS_TAB, [
223✔
761
            named_table,
762
            set,
763
            public,
764
            {keypos, #chain.name},
765
            {read_concurrency, true}
766
        ]),
767
        ok
222✔
768
    catch
769
        error:badarg -> ok
1✔
770
    end.
771

772
global_chain(mqtt) ->
773
    'mqtt:global';
407✔
774
global_chain('mqtt-sn') ->
775
    'mqtt-sn:global';
2✔
776
global_chain(coap) ->
777
    'coap:global';
2✔
778
global_chain(lwm2m) ->
779
    'lwm2m:global';
3✔
780
global_chain(stomp) ->
781
    'stomp:global';
2✔
782
global_chain(_) ->
783
    'unknown:global'.
2✔
784

785
maybe_hook(#{hooked := false} = State) ->
786
    case
523✔
787
        lists:any(
788
            fun
789
                (#chain{authenticators = []}) -> false;
×
790
                (_) -> true
280✔
791
            end,
792
            ets:tab2list(?CHAINS_TAB)
793
        )
794
    of
795
        true ->
796
            ok = emqx_hooks:put('client.authenticate', {?MODULE, authenticate, []}, ?HP_AUTHN),
280✔
797
            State#{hooked => true};
280✔
798
        false ->
799
            State
243✔
800
    end;
801
maybe_hook(State) ->
802
    State.
59✔
803

804
maybe_unhook(#{hooked := true} = State) ->
805
    case
280✔
806
        lists:all(
807
            fun
808
                (#chain{authenticators = []}) -> true;
×
809
                (_) -> false
23✔
810
            end,
811
            ets:tab2list(?CHAINS_TAB)
812
        )
813
    of
814
        true ->
815
            ok = emqx_hooks:del('client.authenticate', {?MODULE, authenticate, []}),
257✔
816
            State#{hooked => false};
257✔
817
        false ->
818
            State
23✔
819
    end;
820
maybe_unhook(State) ->
821
    State.
4✔
822

823
hook_deny() ->
824
    ok = emqx_hooks:put('client.authenticate', {?MODULE, authenticate_deny, []}, ?HP_AUTHN).
223✔
825

826
unhook_deny() ->
827
    ok = emqx_hooks:del('client.authenticate', {?MODULE, authenticate_deny, []}).
223✔
828

829
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
830
    Type = authn_type(Config),
358✔
831
    case maps:get(Type, Providers, undefined) of
358✔
832
        undefined ->
833
            {error, {no_available_provider_for, Type}};
1✔
834
        Provider ->
835
            case Provider:create(AuthenticatorID, Config) of
357✔
836
                {ok, State} ->
837
                    Authenticator = #authenticator{
339✔
838
                        id = AuthenticatorID,
839
                        provider = Provider,
840
                        enable = Enable,
841
                        state = State
842
                    },
843
                    {ok, Authenticator};
339✔
844
                {error, Reason} ->
845
                    {error, Reason}
5✔
846
            end
847
    end.
848

849
do_delete_authenticators(MatchFun, #chain{name = Name, authenticators = Authenticators} = Chain) ->
850
    {Matching, Others} = lists:partition(MatchFun, Authenticators),
279✔
851

852
    MatchingIDs = lists:map(
279✔
853
        fun(#authenticator{id = ID}) -> ID end,
278✔
854
        Matching
855
    ),
856

857
    ok = lists:foreach(
279✔
858
        fun(#authenticator{id = ID} = Authenticator) ->
859
            do_destroy_authenticator(Authenticator),
278✔
860
            emqx_metrics_worker:clear_metrics(authn_metrics, metrics_id(Name, ID))
278✔
861
        end,
862
        Matching
863
    ),
864
    {MatchingIDs, Chain#chain{authenticators = Others}}.
279✔
865

866
do_destroy_authenticator(#authenticator{provider = Provider, state = State}) ->
867
    _ = Provider:destroy(State),
278✔
868
    ok.
278✔
869

870
replace_authenticator(ID, Authenticator, Authenticators) ->
871
    lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator).
67✔
872

873
do_move_authenticator(ID, Authenticators, Position) ->
874
    case lists:keytake(ID, #authenticator.id, Authenticators) of
10✔
875
        false ->
876
            {error, {not_found, {authenticator, ID}}};
×
877
        {value, Authenticator, NAuthenticators} ->
878
            case Position of
10✔
879
                ?CMD_MOVE_FRONT ->
880
                    {ok, [Authenticator | NAuthenticators]};
4✔
881
                ?CMD_MOVE_REAR ->
882
                    {ok, NAuthenticators ++ [Authenticator]};
2✔
883
                ?CMD_MOVE_BEFORE(RelatedID) ->
884
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_BEFORE(RelatedID), []);
2✔
885
                ?CMD_MOVE_AFTER(RelatedID) ->
886
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_AFTER(RelatedID), [])
2✔
887
            end
888
    end.
889

890
insert(_, [], {_, RelatedID}, _) ->
891
    {error, {not_found, {authenticator, RelatedID}}};
×
892
insert(
893
    Authenticator,
894
    [#authenticator{id = RelatedID} = Related | Rest],
895
    {Relative, RelatedID},
896
    Acc
897
) ->
898
    case Relative of
4✔
899
        before ->
900
            {ok, lists:reverse(Acc) ++ [Authenticator, Related | Rest]};
2✔
901
        'after' ->
902
            {ok, lists:reverse(Acc) ++ [Related, Authenticator | Rest]}
2✔
903
    end;
904
insert(Authenticator, [Authenticator0 | More], {Relative, RelatedID}, Acc) ->
905
    insert(Authenticator, More, {Relative, RelatedID}, [Authenticator0 | Acc]).
1✔
906

907
with_new_chain(ChainName, Fun) ->
908
    case ets:lookup(?CHAINS_TAB, ChainName) of
359✔
909
        [] ->
910
            Chain = #chain{name = ChainName, authenticators = []},
310✔
911
            do_with_chain(Fun, Chain);
310✔
912
        [Chain] ->
913
            do_with_chain(Fun, Chain)
49✔
914
    end.
915

916
with_chain(ChainName, Fun) ->
917
    case ets:lookup(?CHAINS_TAB, ChainName) of
440✔
918
        [] ->
919
            {error, {not_found, {chain, ChainName}}};
9✔
920
        [Chain] ->
921
            do_with_chain(Fun, Chain)
431✔
922
    end.
923

924
do_with_chain(Fun, Chain) ->
925
    try
790✔
926
        case Fun(Chain) of
790✔
927
            {ok, Result} ->
928
                Result;
51✔
929
            {ok, Result, NewChain} ->
930
                save_chain(NewChain),
719✔
931
                Result;
719✔
932
            {error, _} = Error ->
933
                Error
7✔
934
        end
935
    catch
936
        Class:Reason:Stk ->
937
            {error, {exception, {Class, Reason, Stk}}}
13✔
938
    end.
939

940
call_authenticator(ChainName, AuthenticatorID, Func, Args) ->
941
    Fun =
53✔
942
        fun(#chain{authenticators = Authenticators}) ->
943
            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
51✔
944
                false ->
945
                    {error, {not_found, {authenticator, AuthenticatorID}}};
×
946
                #authenticator{provider = Provider, state = State} ->
947
                    case erlang:function_exported(Provider, Func, length(Args) + 1) of
51✔
948
                        true ->
949
                            {ok, erlang:apply(Provider, Func, Args ++ [State])};
51✔
950
                        false ->
951
                            {error, unsupported_operation}
×
952
                    end
953
            end
954
        end,
955
    with_chain(ChainName, Fun).
53✔
956

957
serialize_chain(#chain{
958
    name = Name,
959
    authenticators = Authenticators
960
}) ->
961
    #{
15✔
962
        name => Name,
963
        authenticators => serialize_authenticators(Authenticators)
964
    }.
965

966
serialize_authenticators(Authenticators) ->
967
    [serialize_authenticator(Authenticator) || Authenticator <- Authenticators].
347✔
968

969
serialize_authenticator(#authenticator{
970
    id = ID,
971
    provider = Provider,
972
    enable = Enable,
973
    state = State
974
}) ->
975
    #{
925✔
976
        id => ID,
977
        provider => Provider,
978
        enable => Enable,
979
        state => State
980
    }.
981

982
authn_type(#{mechanism := Mechanism, backend := Backend}) ->
983
    {Mechanism, Backend};
330✔
984
authn_type(#{mechanism := Mechanism}) ->
985
    Mechanism.
28✔
986

987
insert_user_group(
988
    Chain,
989
    Config = #{
990
        mechanism := password_based,
991
        backend := built_in_database
992
    }
993
) ->
994
    Config#{user_group => Chain#chain.name};
74✔
995
insert_user_group(_Chain, Config) ->
996
    Config.
351✔
997

998
metrics_id(ChainName, AuthenticatorId) ->
999
    iolist_to_binary([atom_to_binary(ChainName), <<"-">>, AuthenticatorId]).
1,182✔
1000

1001
chain_configs() ->
1002
    [global_chain_config() | listener_chain_configs()].
228✔
1003

1004
global_chain_config() ->
1005
    {?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
228✔
1006

1007
listener_chain_configs() ->
1008
    lists:map(
228✔
1009
        fun({ListenerID, _}) ->
1010
            {ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
914✔
1011
        end,
1012
        emqx_listeners:list()
1013
    ).
1014

1015
auth_config_path(ListenerID) ->
1016
    Names = [
914✔
1017
        binary_to_existing_atom(N, utf8)
1,828✔
1018
     || N <- binary:split(atom_to_binary(ListenerID), <<":">>)
914✔
1019
    ],
1020
    [listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
914✔
1021

1022
to_list(undefined) -> [];
×
1023
to_list(M) when M =:= #{} -> [];
×
1024
to_list(M) when is_map(M) -> [M];
×
1025
to_list(L) when is_list(L) -> L.
1,145✔
1026

1027
call(Call) -> gen_server:call(?MODULE, Call, infinity).
3,093✔
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