Просмотр исходного кода

Merge pull request #10783 from zhongwencool/improve-authn

feat: load changes to the authentication configuration from command line
zhongwencool 2 лет назад
Родитель
Сommit
dd85c981cd

+ 1 - 0
apps/emqx/include/emqx_authentication.hrl

@@ -20,6 +20,7 @@
 -include_lib("emqx/include/logger.hrl").
 
 -define(AUTHN_TRACE_TAG, "AUTHN").
+-define(GLOBAL, 'mqtt:global').
 
 -define(TRACE_AUTHN_PROVIDER(Msg), ?TRACE_AUTHN_PROVIDER(Msg, #{})).
 -define(TRACE_AUTHN_PROVIDER(Msg, Meta), ?TRACE_AUTHN_PROVIDER(debug, Msg, Meta)).

+ 35 - 15
apps/emqx/src/emqx_authentication.erl

@@ -60,7 +60,8 @@
     update_authenticator/3,
     lookup_authenticator/2,
     list_authenticators/1,
-    move_authenticator/3
+    move_authenticator/3,
+    reorder_authenticator/2
 ]).
 
 %% APIs for observer built_in_database
@@ -86,12 +87,6 @@
 %% utility functions
 -export([authenticator_id/1, metrics_id/2]).
 
-%% proxy callback
--export([
-    pre_config_update/3,
-    post_config_update/5
-]).
-
 -export_type([
     authenticator_id/0,
     position/0,
@@ -275,12 +270,6 @@ get_enabled(Authenticators) ->
 %% APIs
 %%------------------------------------------------------------------------------
 
-pre_config_update(Path, UpdateReq, OldConfig) ->
-    emqx_authentication_config:pre_config_update(Path, UpdateReq, OldConfig).
-
-post_config_update(Path, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
-    emqx_authentication_config:post_config_update(Path, UpdateReq, NewConfig, OldConfig, AppEnvs).
-
 %% @doc Get all registered authentication providers.
 get_providers() ->
     call(get_providers).
@@ -413,6 +402,12 @@ list_authenticators(ChainName) ->
 move_authenticator(ChainName, AuthenticatorID, Position) ->
     call({move_authenticator, ChainName, AuthenticatorID, Position}).
 
+-spec reorder_authenticator(chain_name(), [authenticator_id()]) -> ok.
+reorder_authenticator(_ChainName, []) ->
+    ok;
+reorder_authenticator(ChainName, AuthenticatorIDs) ->
+    call({reorder_authenticator, ChainName, AuthenticatorIDs}).
+
 -spec import_users(chain_name(), authenticator_id(), {binary(), binary()}) ->
     ok | {error, term()}.
 import_users(ChainName, AuthenticatorID, Filename) ->
@@ -447,8 +442,9 @@ list_users(ChainName, AuthenticatorID, FuzzyParams) ->
 
 init(_Opts) ->
     process_flag(trap_exit, true),
-    ok = emqx_config_handler:add_handler([?CONF_ROOT], ?MODULE),
-    ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], ?MODULE),
+    Module = emqx_authentication_config,
+    ok = emqx_config_handler:add_handler([?CONF_ROOT], Module),
+    ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], Module),
     {ok, #{hooked => false, providers => #{}}}.
 
 handle_call(get_providers, _From, #{providers := Providers} = State) ->
@@ -504,6 +500,12 @@ handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, S
     end,
     Reply = with_chain(ChainName, UpdateFun),
     reply(Reply, State);
+handle_call({reorder_authenticator, ChainName, AuthenticatorIDs}, _From, State) ->
+    UpdateFun = fun(Chain) ->
+        handle_reorder_authenticator(Chain, AuthenticatorIDs)
+    end,
+    Reply = with_chain(ChainName, UpdateFun),
+    reply(Reply, State);
 handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) ->
     Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]),
     reply(Reply, State);
@@ -609,6 +611,24 @@ handle_move_authenticator(Chain, AuthenticatorID, Position) ->
             {error, Reason}
     end.
 
+handle_reorder_authenticator(Chain, AuthenticatorIDs) ->
+    #chain{authenticators = Authenticators} = Chain,
+    NAuthenticators =
+        lists:filtermap(
+            fun(ID) ->
+                case lists:keyfind(ID, #authenticator.id, Authenticators) of
+                    false ->
+                        ?SLOG(error, #{msg => "authenticator_not_found", id => ID}),
+                        false;
+                    Authenticator ->
+                        {true, Authenticator}
+                end
+            end,
+            AuthenticatorIDs
+        ),
+    NewChain = Chain#chain{authenticators = NAuthenticators},
+    {ok, ok, NewChain}.
+
 handle_create_authenticator(Chain, Config, Providers) ->
     #chain{name = Name, authenticators = Authenticators} = Chain,
     AuthenticatorID = authenticator_id(Config),

+ 81 - 15
apps/emqx/src/emqx_authentication_config.erl

@@ -65,8 +65,8 @@
 
 -spec pre_config_update(list(atom()), update_request(), emqx_config:raw_config()) ->
     {ok, map() | list()} | {error, term()}.
-pre_config_update(_, UpdateReq, OldConfig) ->
-    try do_pre_config_update(UpdateReq, to_list(OldConfig)) of
+pre_config_update(Paths, UpdateReq, OldConfig) ->
+    try do_pre_config_update(Paths, UpdateReq, to_list(OldConfig)) of
         {error, Reason} -> {error, Reason};
         {ok, NewConfig} -> {ok, NewConfig}
     catch
@@ -74,9 +74,9 @@ pre_config_update(_, UpdateReq, OldConfig) ->
             {error, Reason}
     end.
 
-do_pre_config_update({create_authenticator, ChainName, Config}, OldConfig) ->
+do_pre_config_update(_, {create_authenticator, ChainName, Config}, OldConfig) ->
     NewId = authenticator_id(Config),
-    case lists:filter(fun(OldConfig0) -> authenticator_id(OldConfig0) =:= NewId end, OldConfig) of
+    case filter_authenticator(NewId, OldConfig) of
         [] ->
             CertsDir = certs_dir(ChainName, Config),
             NConfig = convert_certs(CertsDir, Config),
@@ -84,7 +84,7 @@ do_pre_config_update({create_authenticator, ChainName, Config}, OldConfig) ->
         [_] ->
             {error, {already_exists, {authenticator, NewId}}}
     end;
-do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
+do_pre_config_update(_, {delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
     NewConfig = lists:filter(
         fun(OldConfig0) ->
             AuthenticatorID =/= authenticator_id(OldConfig0)
@@ -92,7 +92,7 @@ do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldCon
         OldConfig
     ),
     {ok, NewConfig};
-do_pre_config_update({update_authenticator, ChainName, AuthenticatorID, Config}, OldConfig) ->
+do_pre_config_update(_, {update_authenticator, ChainName, AuthenticatorID, Config}, OldConfig) ->
     CertsDir = certs_dir(ChainName, AuthenticatorID),
     NewConfig = lists:map(
         fun(OldConfig0) ->
@@ -104,7 +104,7 @@ do_pre_config_update({update_authenticator, ChainName, AuthenticatorID, Config},
         OldConfig
     ),
     {ok, NewConfig};
-do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
+do_pre_config_update(_, {move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
     case split_by_id(AuthenticatorID, OldConfig) of
         {error, Reason} ->
             {error, Reason};
@@ -129,7 +129,18 @@ do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}
                             {ok, BeforeNFound ++ [FoundRelated, Found | AfterNFound]}
                     end
             end
-    end.
+    end;
+do_pre_config_update(_, OldConfig, OldConfig) ->
+    {ok, OldConfig};
+do_pre_config_update(Paths, NewConfig, _OldConfig) ->
+    ChainName = chain_name(Paths),
+    {ok, [
+        begin
+            CertsDir = certs_dir(ChainName, New),
+            convert_certs(CertsDir, New)
+        end
+     || New <- to_list(NewConfig)
+    ]}.
 
 -spec post_config_update(
     list(atom()),
@@ -139,13 +150,16 @@ do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}
     emqx_config:app_envs()
 ) ->
     ok | {ok, map()} | {error, term()}.
-post_config_update(_, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
-    do_post_config_update(UpdateReq, to_list(NewConfig), OldConfig, AppEnvs).
+post_config_update(Paths, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
+    do_post_config_update(Paths, UpdateReq, to_list(NewConfig), OldConfig, AppEnvs).
 
-do_post_config_update({create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs) ->
+do_post_config_update(
+    _, {create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs
+) ->
     NConfig = get_authenticator_config(authenticator_id(Config), NewConfig),
     emqx_authentication:create_authenticator(ChainName, NConfig);
 do_post_config_update(
+    _,
     {delete_authenticator, ChainName, AuthenticatorID},
     _NewConfig,
     OldConfig,
@@ -160,6 +174,7 @@ do_post_config_update(
             {error, Reason}
     end;
 do_post_config_update(
+    _,
     {update_authenticator, ChainName, AuthenticatorID, Config},
     NewConfig,
     _OldConfig,
@@ -172,12 +187,57 @@ do_post_config_update(
             emqx_authentication:update_authenticator(ChainName, AuthenticatorID, NConfig)
     end;
 do_post_config_update(
+    _,
     {move_authenticator, ChainName, AuthenticatorID, Position},
     _NewConfig,
     _OldConfig,
     _AppEnvs
 ) ->
-    emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position).
+    emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position);
+do_post_config_update(_, _UpdateReq, OldConfig, OldConfig, _AppEnvs) ->
+    ok;
+do_post_config_update(Paths, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) ->
+    ChainName = chain_name(Paths),
+    OldConfig = to_list(OldConfig0),
+    NewConfig = to_list(NewConfig0),
+    OldIds = lists:map(fun authenticator_id/1, OldConfig),
+    NewIds = lists:map(fun authenticator_id/1, NewConfig),
+    ok = delete_authenticators(NewIds, ChainName, OldConfig),
+    ok = create_or_update_authenticators(OldIds, ChainName, NewConfig),
+    ok = emqx_authentication:reorder_authenticator(ChainName, NewIds),
+    ok.
+
+%% create new authenticators and update existing ones
+create_or_update_authenticators(OldIds, ChainName, NewConfig) ->
+    lists:foreach(
+        fun(Conf) ->
+            Id = authenticator_id(Conf),
+            case lists:member(Id, OldIds) of
+                true ->
+                    emqx_authentication:update_authenticator(ChainName, Id, Conf);
+                false ->
+                    emqx_authentication:create_authenticator(ChainName, Conf)
+            end
+        end,
+        NewConfig
+    ).
+
+%% delete authenticators that are not in the new config
+delete_authenticators(NewIds, ChainName, OldConfig) ->
+    lists:foreach(
+        fun(Conf) ->
+            Id = authenticator_id(Conf),
+            case lists:member(Id, NewIds) of
+                true ->
+                    ok;
+                false ->
+                    _ = emqx_authentication:delete_authenticator(ChainName, Id),
+                    CertsDir = certs_dir(ChainName, Conf),
+                    ok = clear_certs(CertsDir, Conf)
+            end
+        end,
+        OldConfig
+    ).
 
 to_list(undefined) -> [];
 to_list(M) when M =:= #{} -> [];
@@ -213,14 +273,15 @@ clear_certs(CertsDir, Config) ->
     ok = emqx_tls_lib:delete_ssl_files(CertsDir, undefined, OldSSL).
 
 get_authenticator_config(AuthenticatorID, AuthenticatorsConfig) ->
-    case
-        lists:filter(fun(C) -> AuthenticatorID =:= authenticator_id(C) end, AuthenticatorsConfig)
-    of
+    case filter_authenticator(AuthenticatorID, AuthenticatorsConfig) of
         [C] -> C;
         [] -> {error, not_found};
         _ -> error({duplicated_authenticator_id, AuthenticatorsConfig})
     end.
 
+filter_authenticator(ID, Authenticators) ->
+    lists:filter(fun(A) -> ID =:= authenticator_id(A) end, Authenticators).
+
 split_by_id(ID, AuthenticatorsConfig) ->
     case
         lists:foldl(
@@ -287,3 +348,8 @@ dir(ChainName, ID) when is_binary(ID) ->
     emqx_utils:safe_filename(iolist_to_binary([to_bin(ChainName), "-", ID]));
 dir(ChainName, Config) when is_map(Config) ->
     dir(ChainName, authenticator_id(Config)).
+
+chain_name([authentication]) ->
+    ?GLOBAL;
+chain_name([listeners, Type, Name, authentication]) ->
+    binary_to_existing_atom(<<(atom_to_binary(Type))/binary, ":", (atom_to_binary(Name))/binary>>).

+ 6 - 4
apps/emqx/src/emqx_config.erl

@@ -288,12 +288,14 @@ get_default_value([RootName | _] = KeyPath) ->
     end.
 
 -spec get_raw(emqx_utils_maps:config_key_path()) -> term().
-get_raw([Root | T]) when is_atom(Root) -> get_raw([bin(Root) | T]);
-get_raw(KeyPath) -> do_get_raw(KeyPath).
+get_raw([Root | _] = KeyPath) when is_binary(Root) -> do_get_raw(KeyPath);
+get_raw([Root | T]) -> get_raw([bin(Root) | T]);
+get_raw([]) -> do_get_raw([]).
 
 -spec get_raw(emqx_utils_maps:config_key_path(), term()) -> term().
-get_raw([Root | T], Default) when is_atom(Root) -> get_raw([bin(Root) | T], Default);
-get_raw(KeyPath, Default) -> do_get_raw(KeyPath, Default).
+get_raw([Root | _] = KeyPath, Default) when is_binary(Root) -> do_get_raw(KeyPath, Default);
+get_raw([Root | T], Default) -> get_raw([bin(Root) | T], Default);
+get_raw([], Default) -> do_get_raw([], Default).
 
 -spec put_raw(map()) -> ok.
 put_raw(Config) ->

+ 15 - 4
apps/emqx/test/emqx_authentication_SUITE.erl

@@ -174,7 +174,7 @@ t_authenticator(Config) when is_list(Config) ->
     register_provider(AuthNType1, ?MODULE),
     ID1 = <<"password_based:built_in_database">>,
 
-    % CRUD of authencaticator
+    % CRUD of authenticator
     ?assertMatch(
         {ok, #{id := ID1, state := #{mark := 1}}},
         ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
@@ -296,8 +296,10 @@ t_update_config({init, Config}) ->
         | Config
     ];
 t_update_config(Config) when is_list(Config) ->
-    emqx_config_handler:add_handler([?CONF_ROOT], emqx_authentication),
-    ok = emqx_config_handler:add_handler([listeners, '?', '?', ?CONF_ROOT], emqx_authentication),
+    emqx_config_handler:add_handler([?CONF_ROOT], emqx_authentication_config),
+    ok = emqx_config_handler:add_handler(
+        [listeners, '?', '?', ?CONF_ROOT], emqx_authentication_config
+    ),
     ok = register_provider(?config("auth1"), ?MODULE),
     ok = register_provider(?config("auth2"), ?MODULE),
     Global = ?config(global),
@@ -356,6 +358,10 @@ t_update_config(Config) when is_list(Config) ->
 
     ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)),
 
+    [Raw2, Raw1] = emqx:get_raw_config([?CONF_ROOT]),
+    ?assertMatch({ok, _}, update_config([?CONF_ROOT], [Raw1, Raw2])),
+    ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(Global)),
+
     ?assertMatch({ok, _}, update_config([?CONF_ROOT], {delete_authenticator, Global, ID1})),
     ?assertEqual(
         {error, {not_found, {authenticator, ID1}}},
@@ -418,11 +424,16 @@ t_update_config(Config) when is_list(Config) ->
         {ok, _},
         update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, ?CMD_MOVE_FRONT})
     ),
-
     ?assertMatch(
         {ok, [#{id := ID2}, #{id := ID1}]},
         ?AUTHN:list_authenticators(ListenerID)
     ),
+    [LRaw2, LRaw1] = emqx:get_raw_config(ConfKeyPath),
+    ?assertMatch({ok, _}, update_config(ConfKeyPath, [LRaw1, LRaw2])),
+    ?assertMatch(
+        {ok, [#{id := ID1}, #{id := ID2}]},
+        ?AUTHN:list_authenticators(ListenerID)
+    ),
 
     ?assertMatch(
         {ok, _},

+ 0 - 2
apps/emqx_authn/include/emqx_authn.hrl

@@ -23,8 +23,6 @@
 
 -define(AUTHN, emqx_authentication).
 
--define(GLOBAL, 'mqtt:global').
-
 -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
 
 -define(AUTH_SHARD, emqx_authn_shard).

+ 1 - 1
apps/emqx_authn/src/emqx_authn.app.src

@@ -1,7 +1,7 @@
 %% -*- mode: erlang -*-
 {application, emqx_authn, [
     {description, "EMQX Authentication"},
-    {vsn, "0.1.20"},
+    {vsn, "0.1.21"},
     {modules, []},
     {registered, [emqx_authn_sup, emqx_authn_registry]},
     {applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]},

+ 6 - 5
apps/emqx_authn/src/emqx_authn_api.erl

@@ -31,6 +31,7 @@
 -define(NOT_FOUND, 'NOT_FOUND').
 -define(ALREADY_EXISTS, 'ALREADY_EXISTS').
 -define(INTERNAL_ERROR, 'INTERNAL_ERROR').
+-define(CONFIG, emqx_authentication_config).
 
 % Swagger
 
@@ -833,12 +834,12 @@ with_chain(ListenerID, Fun) ->
 create_authenticator(ConfKeyPath, ChainName, Config) ->
     case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of
         {ok, #{
-            post_config_update := #{emqx_authentication := #{id := ID}},
+            post_config_update := #{?CONFIG := #{id := ID}},
             raw_config := AuthenticatorsConfig
         }} ->
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
             {200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
-        {error, {_PrePostConfigUpdate, emqx_authentication, Reason}} ->
+        {error, {_PrePostConfigUpdate, ?CONFIG, Reason}} ->
             serialize_error(Reason);
         {error, Reason} ->
             serialize_error(Reason)
@@ -1017,7 +1018,7 @@ update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) ->
     of
         {ok, _} ->
             {204};
-        {error, {_PrePostConfigUpdate, emqx_authentication, Reason}} ->
+        {error, {_PrePostConfigUpdate, ?CONFIG, Reason}} ->
             serialize_error(Reason);
         {error, Reason} ->
             serialize_error(Reason)
@@ -1027,7 +1028,7 @@ delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) ->
     case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of
         {ok, _} ->
             {204};
-        {error, {_PrePostConfigUpdate, emqx_authentication, Reason}} ->
+        {error, {_PrePostConfigUpdate, ?CONFIG, Reason}} ->
             serialize_error(Reason);
         {error, Reason} ->
             serialize_error(Reason)
@@ -1044,7 +1045,7 @@ move_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Position) ->
             of
                 {ok, _} ->
                     {204};
-                {error, {_PrePostConfigUpdate, emqx_authentication, Reason}} ->
+                {error, {_PrePostConfigUpdate, ?CONFIG, Reason}} ->
                     serialize_error(Reason);
                 {error, Reason} ->
                     serialize_error(Reason)

+ 121 - 0
apps/emqx_authn/test/emqx_authn_SUITE.erl

@@ -200,6 +200,127 @@ t_union_selector_errors(Config) when is_list(Config) ->
     ),
     ok.
 
+t_update_conf({init, Config}) ->
+    emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]),
+    {ok, _} = emqx:update_config([authentication], []),
+    Config;
+t_update_conf({'end', _Config}) ->
+    {ok, _} = emqx:update_config([authentication], []),
+    emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]),
+    ok;
+t_update_conf(Config) when is_list(Config) ->
+    Authn1 = #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"built_in_database">>,
+        <<"user_id_type">> => <<"clientid">>,
+        <<"enable">> => true
+    },
+    Authn2 = #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"http">>,
+        <<"method">> => <<"post">>,
+        <<"url">> => <<"http://127.0.0.1:18083">>,
+        <<"headers">> => #{
+            <<"content-type">> => <<"application/json">>
+        },
+        <<"enable">> => true
+    },
+    Authn3 = #{
+        <<"mechanism">> => <<"jwt">>,
+        <<"use_jwks">> => false,
+        <<"algorithm">> => <<"hmac-based">>,
+        <<"secret">> => <<"mysecret">>,
+        <<"secret_base64_encoded">> => false,
+        <<"verify_claims">> => #{<<"username">> => <<"${username}">>},
+        <<"enable">> => true
+    },
+    Chain = 'mqtt:global',
+    {ok, _} = emqx:update_config([authentication], [Authn1]),
+    ?assertMatch(
+        {ok, #{
+            authenticators := [
+                #{
+                    enable := true,
+                    id := <<"password_based:built_in_database">>,
+                    provider := emqx_authn_mnesia
+                }
+            ]
+        }},
+        emqx_authentication:lookup_chain(Chain)
+    ),
+
+    {ok, _} = emqx:update_config([authentication], [Authn1, Authn2, Authn3]),
+    ?assertMatch(
+        {ok, #{
+            authenticators := [
+                #{
+                    enable := true,
+                    id := <<"password_based:built_in_database">>,
+                    provider := emqx_authn_mnesia
+                },
+                #{
+                    enable := true,
+                    id := <<"password_based:http">>,
+                    provider := emqx_authn_http
+                },
+                #{
+                    enable := true,
+                    id := <<"jwt">>,
+                    provider := emqx_authn_jwt
+                }
+            ]
+        }},
+        emqx_authentication:lookup_chain(Chain)
+    ),
+    {ok, _} = emqx:update_config([authentication], [Authn2, Authn1]),
+    ?assertMatch(
+        {ok, #{
+            authenticators := [
+                #{
+                    enable := true,
+                    id := <<"password_based:http">>,
+                    provider := emqx_authn_http
+                },
+                #{
+                    enable := true,
+                    id := <<"password_based:built_in_database">>,
+                    provider := emqx_authn_mnesia
+                }
+            ]
+        }},
+        emqx_authentication:lookup_chain(Chain)
+    ),
+
+    {ok, _} = emqx:update_config([authentication], [Authn3, Authn2, Authn1]),
+    ?assertMatch(
+        {ok, #{
+            authenticators := [
+                #{
+                    enable := true,
+                    id := <<"jwt">>,
+                    provider := emqx_authn_jwt
+                },
+                #{
+                    enable := true,
+                    id := <<"password_based:http">>,
+                    provider := emqx_authn_http
+                },
+                #{
+                    enable := true,
+                    id := <<"password_based:built_in_database">>,
+                    provider := emqx_authn_mnesia
+                }
+            ]
+        }},
+        emqx_authentication:lookup_chain(Chain)
+    ),
+    {ok, _} = emqx:update_config([authentication], []),
+    ?assertMatch(
+        {error, {not_found, {chain, Chain}}},
+        emqx_authentication:lookup_chain(Chain)
+    ),
+    ok.
+
 parse(Bytes) ->
     {ok, Frame, <<>>, {none, _}} = emqx_frame:parse(Bytes),
     Frame.

+ 55 - 4
apps/emqx_conf/src/emqx_conf_cli.erl

@@ -18,16 +18,40 @@
 -export([
     load/0,
     admins/1,
+    conf/1,
     unload/0
 ]).
 
--define(CMD, cluster_call).
+-define(CLUSTER_CALL, cluster_call).
+-define(CONF, conf).
 
 load() ->
-    emqx_ctl:register_command(?CMD, {?MODULE, admins}, []).
+    emqx_ctl:register_command(?CLUSTER_CALL, {?MODULE, admins}, []),
+    emqx_ctl:register_command(?CONF, {?MODULE, conf}, []).
 
 unload() ->
-    emqx_ctl:unregister_command(?CMD).
+    emqx_ctl:unregister_command(?CLUSTER_CALL),
+    emqx_ctl:unregister_command(?CONF).
+
+conf(["show", "--keys-only"]) ->
+    print(emqx_config:get_root_names());
+conf(["show"]) ->
+    print_hocon(get_config());
+conf(["show", Key]) ->
+    print_hocon(get_config(Key));
+conf(["load", Path]) ->
+    load_config(Path);
+conf(_) ->
+    emqx_ctl:usage(
+        [
+            %% TODO add reload
+            %{"conf reload", "reload etc/emqx.conf on local node"},
+            {"conf show --keys-only", "print all keys"},
+            {"conf show", "print all running configures"},
+            {"conf show <key>", "print a specific configuration"},
+            {"conf load <path>", "load a hocon file to all nodes"}
+        ]
+    ).
 
 admins(["status"]) ->
     status();
@@ -43,7 +67,7 @@ admins(["skip", Node0]) ->
     status();
 admins(["tnxid", TnxId0]) ->
     TnxId = list_to_integer(TnxId0),
-    emqx_ctl:print("~p~n", [emqx_cluster_rpc:query(TnxId)]);
+    print(emqx_cluster_rpc:query(TnxId));
 admins(["fast_forward"]) ->
     status(),
     Nodes = mria:running_nodes(),
@@ -91,3 +115,30 @@ status() ->
         Status
     ),
     emqx_ctl:print("-----------------------------------------------\n").
+
+print(Json) ->
+    emqx_ctl:print("~ts~n", [emqx_logger_jsonfmt:best_effort_json(Json)]).
+
+print_hocon(Hocon) ->
+    emqx_ctl:print("~ts~n", [hocon_pp:do(Hocon, #{})]).
+
+get_config() -> emqx_config:fill_defaults(emqx:get_raw_config([])).
+get_config(Key) -> emqx_config:fill_defaults(#{Key => emqx:get_raw_config([Key])}).
+
+-define(OPTIONS, #{rawconf_with_defaults => true, override_to => cluster}).
+load_config(Path) ->
+    case hocon:files([Path]) of
+        {ok, Conf} ->
+            maps:foreach(
+                fun(Key, Value) ->
+                    case emqx_conf:update([Key], Value, ?OPTIONS) of
+                        {ok, _} -> emqx_ctl:print("load ~ts ok~n", [Key]);
+                        {error, Reason} -> emqx_ctl:print("load ~ts failed: ~p~n", [Key, Reason])
+                    end
+                end,
+                Conf
+            );
+        {error, Reason} ->
+            emqx_ctl:print("load ~ts failed~n~p~n", [Path, Reason]),
+            {error, bad_hocon_file}
+    end.

+ 1 - 1
apps/emqx_ctl/src/emqx_ctl.app.src

@@ -1,6 +1,6 @@
 {application, emqx_ctl, [
     {description, "Backend for emqx_ctl script"},
-    {vsn, "0.1.1"},
+    {vsn, "0.1.2"},
     {registered, []},
     {mod, {emqx_ctl_app, []}},
     {applications, [

+ 30 - 17
apps/emqx_ctl/src/emqx_ctl.erl

@@ -128,16 +128,21 @@ run_command(Cmd, Args) when is_atom(Cmd) ->
                     }),
                     {error, Reason}
             end;
-        [] ->
+        Error ->
             help(),
-            {error, cmd_not_found}
+            Error
     end.
 
 -spec lookup_command(cmd()) -> [{module(), atom()}].
 lookup_command(Cmd) when is_atom(Cmd) ->
-    case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
-        [El] -> El;
-        [] -> []
+    case is_initialized() of
+        true ->
+            case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of
+                [El] -> El;
+                [] -> {error, cmd_not_found}
+            end;
+        false ->
+            {error, cmd_is_initializing}
     end.
 
 -spec get_commands() -> list({cmd(), module(), atom()}).
@@ -145,18 +150,23 @@ get_commands() ->
     [{Cmd, M, F} || {{_Seq, Cmd}, {M, F}, _Opts} <- ets:tab2list(?CMD_TAB)].
 
 help() ->
-    case ets:tab2list(?CMD_TAB) of
-        [] ->
-            print("No commands available.~n");
-        Cmds ->
-            print("Usage: ~ts~n", ["emqx ctl"]),
-            lists:foreach(
-                fun({_, {Mod, Cmd}, _}) ->
-                    print("~110..-s~n", [""]),
-                    apply(Mod, Cmd, [usage])
-                end,
-                Cmds
-            )
+    case is_initialized() of
+        true ->
+            case ets:tab2list(?CMD_TAB) of
+                [] ->
+                    print("No commands available.~n");
+                Cmds ->
+                    print("Usage: ~ts~n", ["emqx ctl"]),
+                    lists:foreach(
+                        fun({_, {Mod, Cmd}, _}) ->
+                            print("~110..-s~n", [""]),
+                            apply(Mod, Cmd, [usage])
+                        end,
+                        Cmds
+                    )
+            end;
+        false ->
+            print("Command table is initializing.~n")
     end.
 
 -spec print(io:format()) -> ok.
@@ -279,3 +289,6 @@ safe_to_existing_atom(Str) ->
         _:badarg ->
             undefined
     end.
+
+is_initialized() ->
+    ets:info(?CMD_TAB) =/= undefined.

+ 2 - 2
apps/emqx_ctl/test/emqx_ctl_SUITE.erl

@@ -49,8 +49,8 @@ t_reg_unreg_command(_) ->
             emqx_ctl:unregister_command(cmd1),
             emqx_ctl:unregister_command(cmd2),
             ct:sleep(100),
-            ?assertEqual([], emqx_ctl:lookup_command(cmd1)),
-            ?assertEqual([], emqx_ctl:lookup_command(cmd2)),
+            ?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd1)),
+            ?assertEqual({error, cmd_not_found}, emqx_ctl:lookup_command(cmd2)),
             ?assertEqual([], emqx_ctl:get_commands())
         end
     ).