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

emqx / emqx / 12816797250

16 Jan 2025 08:11PM UTC coverage: 82.376%. First build
12816797250

Pull #14565

github

web-flow
Merge bfe875647 into e9cb22316
Pull Request #14565: sync `release-58` to `master`

184 of 216 new or added lines in 17 files covered. (85.19%)

57794 of 70159 relevant lines covered (82.38%)

15208.36 hits per line

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

85.44
/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
]).
50

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

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

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

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

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

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

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

113
-define(CHAINS_TAB, emqx_authn_chains).
114

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

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

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

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

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

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

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

164
%%------------------------------------------------------------------------------
165
%% Authenticate
166
%%------------------------------------------------------------------------------
167

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

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

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

217
%%------------------------------------------------------------------------------
218
%% APIs
219
%%------------------------------------------------------------------------------
220

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

371
%%--------------------------------------------------------------------
372
%% gen_server callbacks
373
%%--------------------------------------------------------------------
374

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

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

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

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

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

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

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

508
%%------------------------------------------------------------------------------
509
%% Private functions
510
%%------------------------------------------------------------------------------
511

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

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

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

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

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

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

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

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

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

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

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

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

675
do_authenticate(_ChainName, [], _) ->
676
    {ok, {error, not_authorized}};
156✔
677
do_authenticate(
678
    ChainName,
679
    [#authenticator{id = ID, provider = _Module} = Authenticator | More],
680
    Credential
681
) ->
682
    MetricsID = metrics_id(ChainName, ID),
453✔
683
    emqx_metrics_worker:inc(authn_metrics, MetricsID, total),
453✔
684

685
    Result = ?EXT_TRACE_WITH_PROCESS_FUN(
453✔
686
        client_authn_backend,
687
        #{
688
            'client.clientid' => maps:get(clientid, Credential, undefined),
689
            'client.username' => maps:get(username, Credential, undefined),
690
            'authn.authenticator' => ID,
691
            'authn.chain' => ChainName,
692
            'authn.module' => _Module
693
        },
694
        fun() ->
179✔
695
            try authenticate_with_provider(Authenticator, Credential) of
453✔
696
                ignore ->
697
                    ok = emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
156✔
698
                    ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ignore}),
156✔
699
                    ignore;
156✔
700
                Res ->
701
                    %% {ok, Extra}
702
                    %% {ok, Extra, AuthData} XXX: should inc metrics success for this clause?
703
                    %% {continue, AuthCache}
704
                    %% {continue, AuthData, AuthCache}
705
                    %% {error, Reason}
706
                    case Res of
297✔
707
                        {ok, _} ->
708
                            emqx_metrics_worker:inc(authn_metrics, MetricsID, success),
214✔
709
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ok});
214✔
710
                        {error, _} ->
711
                            emqx_metrics_worker:inc(authn_metrics, MetricsID, failed),
66✔
712
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => error}),
66✔
713
                            ?EXT_TRACE_SET_STATUS_ERROR();
66✔
714
                        _ ->
715
                            ?EXT_TRACE_ADD_ATTRS(#{'authn.result' => ok}),
17✔
716
                            ok
17✔
717
                    end,
718
                    {stop, Res}
297✔
719
            catch
720
                Class:Reason:Stacktrace ->
NEW
721
                    ?TRACE_AUTHN(
×
NEW
722
                        warning,
×
723
                        "authenticator_error",
NEW
724
                        maybe_add_stacktrace(
×
725
                            Class,
726
                            #{
727
                                exception => Class,
728
                                reason => Reason,
729
                                authenticator => ID
730
                            },
731
                            Stacktrace
NEW
732
                        )
×
733
                    ),
NEW
734
                    emqx_metrics_worker:inc(authn_metrics, MetricsID, nomatch),
×
NEW
735
                    with_provider_failed
×
736
            end
737
        end
738
    ),
739
    case Result of
453✔
NEW
740
        with_provider_failed -> do_authenticate(ChainName, More, Credential);
×
741
        ignore -> do_authenticate(ChainName, More, Credential);
156✔
742
        {stop, _} -> Result
297✔
743
    end.
744

745
maybe_add_stacktrace('throw', Data, _Stacktrace) ->
746
    Data;
×
747
maybe_add_stacktrace(_, Data, Stacktrace) ->
748
    Data#{stacktrace => Stacktrace}.
×
749

750
authenticate_with_provider(#authenticator{id = ID, provider = Provider, state = State}, Credential) ->
751
    AuthnResult = Provider:authenticate(Credential, State),
453✔
752
    ?TRACE_AUTHN("authenticator_result", #{
453✔
753
        authenticator => ID,
754
        result => AuthnResult
755
    }),
453✔
756
    AuthnResult.
453✔
757

758
reply(Reply, State) ->
759
    {reply, Reply, State}.
1,877✔
760

761
reply(Reply, State, Continue) ->
762
    {reply, Reply, State, {continue, Continue}}.
1,246✔
763

764
save_chain(#chain{
765
    name = Name,
766
    authenticators = []
767
}) ->
768
    ets:delete(?CHAINS_TAB, Name);
278✔
769
save_chain(#chain{} = Chain) ->
770
    ets:insert(?CHAINS_TAB, Chain).
461✔
771

772
create_chain_table() ->
773
    try
224✔
774
        _ = ets:new(?CHAINS_TAB, [
224✔
775
            named_table,
776
            set,
777
            public,
778
            {keypos, #chain.name},
779
            {read_concurrency, true}
780
        ]),
781
        ok
223✔
782
    catch
783
        error:badarg -> ok
1✔
784
    end.
785

786
global_chain(mqtt) ->
787
    'mqtt:global';
796✔
788
global_chain('mqtt-sn') ->
789
    'mqtt-sn:global';
72✔
790
global_chain(coap) ->
791
    'coap:global';
19✔
792
global_chain(lwm2m) ->
793
    'lwm2m:global';
41✔
794
global_chain(stomp) ->
795
    'stomp:global';
22✔
796
global_chain(_) ->
797
    'unknown:global'.
78✔
798

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

802
unhook() ->
803
    ok = emqx_hooks:del('client.authenticate', {?MODULE, authenticate, []}).
180✔
804

805
do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) ->
806
    Type = authn_type(Config),
368✔
807
    case maps:get(Type, Providers, undefined) of
368✔
808
        undefined ->
809
            {error, {no_available_provider_for, Type}};
1✔
810
        Provider ->
811
            case Provider:create(AuthenticatorID, Config) of
367✔
812
                {ok, State} ->
813
                    Authenticator = #authenticator{
349✔
814
                        id = AuthenticatorID,
815
                        provider = Provider,
816
                        enable = Enable,
817
                        state = State
818
                    },
819
                    {ok, Authenticator};
349✔
820
                {error, Reason} ->
821
                    {error, Reason}
5✔
822
            end
823
    end.
824

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

828
    MatchingIDs = lists:map(
289✔
829
        fun(#authenticator{id = ID}) -> ID end,
288✔
830
        Matching
831
    ),
832

833
    ok = lists:foreach(
289✔
834
        fun(#authenticator{id = ID} = Authenticator) ->
835
            do_destroy_authenticator(Authenticator),
288✔
836
            emqx_metrics_worker:clear_metrics(authn_metrics, metrics_id(Name, ID))
288✔
837
        end,
838
        Matching
839
    ),
840
    {MatchingIDs, Chain#chain{authenticators = Others}}.
289✔
841

842
do_destroy_authenticator(#authenticator{provider = Provider, state = State}) ->
843
    _ = Provider:destroy(State),
288✔
844
    ok.
288✔
845

846
replace_authenticator(ID, Authenticator, Authenticators) ->
847
    lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator).
67✔
848

849
do_move_authenticator(ID, Authenticators, Position) ->
850
    case lists:keytake(ID, #authenticator.id, Authenticators) of
10✔
851
        false ->
852
            {error, {not_found, {authenticator, ID}}};
×
853
        {value, Authenticator, NAuthenticators} ->
854
            case Position of
10✔
855
                ?CMD_MOVE_FRONT ->
856
                    {ok, [Authenticator | NAuthenticators]};
4✔
857
                ?CMD_MOVE_REAR ->
858
                    {ok, NAuthenticators ++ [Authenticator]};
2✔
859
                ?CMD_MOVE_BEFORE(RelatedID) ->
860
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_BEFORE(RelatedID), []);
2✔
861
                ?CMD_MOVE_AFTER(RelatedID) ->
862
                    insert(Authenticator, NAuthenticators, ?CMD_MOVE_AFTER(RelatedID), [])
2✔
863
            end
864
    end.
865

866
insert(_, [], {_, RelatedID}, _) ->
867
    {error, {not_found, {authenticator, RelatedID}}};
×
868
insert(
869
    Authenticator,
870
    [#authenticator{id = RelatedID} = Related | Rest],
871
    {Relative, RelatedID},
872
    Acc
873
) ->
874
    case Relative of
4✔
875
        before ->
876
            {ok, lists:reverse(Acc) ++ [Authenticator, Related | Rest]};
2✔
877
        'after' ->
878
            {ok, lists:reverse(Acc) ++ [Related, Authenticator | Rest]}
2✔
879
    end;
880
insert(Authenticator, [Authenticator0 | More], {Relative, RelatedID}, Acc) ->
881
    insert(Authenticator, More, {Relative, RelatedID}, [Authenticator0 | Acc]).
1✔
882

883
with_new_chain(ChainName, Fun) ->
884
    case ets:lookup(?CHAINS_TAB, ChainName) of
369✔
885
        [] ->
886
            Chain = #chain{name = ChainName, authenticators = []},
320✔
887
            do_with_chain(Fun, Chain);
320✔
888
        [Chain] ->
889
            do_with_chain(Fun, Chain)
49✔
890
    end.
891

892
with_chain(ChainName, Fun) ->
893
    case ets:lookup(?CHAINS_TAB, ChainName) of
450✔
894
        [] ->
895
            {error, {not_found, {chain, ChainName}}};
9✔
896
        [Chain] ->
897
            do_with_chain(Fun, Chain)
441✔
898
    end.
899

900
do_with_chain(Fun, Chain) ->
901
    try
810✔
902
        case Fun(Chain) of
810✔
903
            {ok, Result} ->
904
                Result;
51✔
905
            {ok, Result, NewChain} ->
906
                save_chain(NewChain),
739✔
907
                Result;
739✔
908
            {error, _} = Error ->
909
                Error
7✔
910
        end
911
    catch
912
        Class:Reason:Stk ->
913
            {error, {exception, {Class, Reason, Stk}}}
13✔
914
    end.
915

916
call_authenticator(ChainName, AuthenticatorID, Func, Args) ->
917
    Fun =
53✔
918
        fun(#chain{authenticators = Authenticators}) ->
919
            case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
51✔
920
                false ->
921
                    {error, {not_found, {authenticator, AuthenticatorID}}};
×
922
                #authenticator{provider = Provider, state = State} ->
923
                    case erlang:function_exported(Provider, Func, length(Args) + 1) of
51✔
924
                        true ->
925
                            {ok, erlang:apply(Provider, Func, Args ++ [State])};
51✔
926
                        false ->
927
                            {error, unsupported_operation}
×
928
                    end
929
            end
930
        end,
931
    with_chain(ChainName, Fun).
53✔
932

933
serialize_chain(#chain{
934
    name = Name,
935
    authenticators = Authenticators
936
}) ->
937
    #{
15✔
938
        name => Name,
939
        authenticators => serialize_authenticators(Authenticators)
940
    }.
941

942
serialize_authenticators(Authenticators) ->
943
    [serialize_authenticator(Authenticator) || Authenticator <- Authenticators].
358✔
944

945
serialize_authenticator(#authenticator{
946
    id = ID,
947
    provider = Provider,
948
    enable = Enable,
949
    state = State
950
}) ->
951
    #{
946✔
952
        id => ID,
953
        provider => Provider,
954
        enable => Enable,
955
        state => State
956
    }.
957

958
authn_type(#{mechanism := Mechanism, backend := Backend}) ->
959
    {Mechanism, Backend};
339✔
960
authn_type(#{mechanism := Mechanism}) ->
961
    Mechanism.
29✔
962

963
insert_user_group(
964
    Chain,
965
    Config = #{
966
        mechanism := password_based,
967
        backend := built_in_database
968
    }
969
) ->
970
    Config#{user_group => Chain#chain.name};
74✔
971
insert_user_group(_Chain, Config) ->
972
    Config.
361✔
973

974
metrics_id(ChainName, AuthenticatorId) ->
975
    iolist_to_binary([atom_to_binary(ChainName), <<"-">>, AuthenticatorId]).
1,237✔
976

977
chain_configs() ->
978
    [global_chain_config() | listener_chain_configs()].
229✔
979

980
global_chain_config() ->
981
    {?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
229✔
982

983
listener_chain_configs() ->
984
    lists:map(
229✔
985
        fun({ListenerID, _}) ->
986
            {ListenerID, emqx:get_config(auth_config_path(ListenerID), [])}
918✔
987
        end,
988
        emqx_listeners:list()
989
    ).
990

991
auth_config_path(ListenerID) ->
992
    Names = [
918✔
993
        binary_to_existing_atom(N, utf8)
1,836✔
994
     || N <- binary:split(atom_to_binary(ListenerID), <<":">>)
918✔
995
    ],
996
    [listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
918✔
997

998
to_list(undefined) -> [];
×
999
to_list(M) when M =:= #{} -> [];
×
1000
to_list(M) when is_map(M) -> [M];
×
1001
to_list(L) when is_list(L) -> L.
1,150✔
1002

1003
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