Преглед изворни кода

Merge pull request #11382 from savonarola/0801-auth-refactor

Auth refactor stage 1
Ilya Averyanov пре 2 година
родитељ
комит
ed67303281
56 измењених фајлова са 1198 додато и 488 уклоњено
  1. 0 16
      apps/emqx/include/emqx.hrl
  2. 7 1
      apps/emqx/include/emqx_access_control.hrl
  3. 33 1
      apps/emqx/src/emqx_access_control.erl
  4. 1 11
      apps/emqx/src/emqx_broker_sup.erl
  5. 267 47
      apps/emqx/src/emqx_config_handler.erl
  6. 6 46
      apps/emqx/src/emqx_listeners.erl
  7. 13 50
      apps/emqx/src/emqx_schema.erl
  8. 118 0
      apps/emqx/src/emqx_schema_hooks.erl
  9. 12 19
      apps/emqx/test/emqx_broker_SUITE.erl
  10. 1 3
      apps/emqx/test/emqx_common_test_helpers.erl
  11. 127 19
      apps/emqx/test/emqx_config_handler_SUITE.erl
  12. 3 3
      apps/emqx/test/emqx_cth_suite.erl
  13. 1 7
      apps/emqx/include/emqx_authentication.hrl
  14. 1 1
      apps/emqx_authn/include/emqx_authn.hrl
  15. 2 0
      apps/emqx_authn/rebar.config
  16. 14 19
      apps/emqx/src/emqx_authentication.erl
  17. 59 21
      apps/emqx/src/emqx_authentication_config.erl
  18. 0 0
      apps/emqx_authn/src/emqx_authentication_sup.erl
  19. 0 1
      apps/emqx_authn/src/emqx_authn_api.erl
  20. 2 3
      apps/emqx_authn/src/emqx_authn_app.erl
  21. 39 4
      apps/emqx_authn/src/emqx_authn_schema.erl
  22. 11 1
      apps/emqx_authn/src/emqx_authn_sup.erl
  23. 0 1
      apps/emqx_authn/src/emqx_authn_user_import_api.erl
  24. 30 17
      apps/emqx/test/emqx_authentication_SUITE.erl
  25. 17 9
      apps/emqx_authn/test/emqx_authn_api_SUITE.erl
  26. 10 6
      apps/emqx_authn/test/emqx_authn_enable_flag_SUITE.erl
  27. 6 7
      apps/emqx_authn/test/emqx_authn_http_SUITE.erl
  28. 6 7
      apps/emqx_authn/test/emqx_authn_https_SUITE.erl
  29. 7 14
      apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl
  30. 242 0
      apps/emqx_authn/test/emqx_authn_listeners_SUITE.erl
  31. 7 8
      apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl
  32. 7 8
      apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl
  33. 7 8
      apps/emqx_authn/test/emqx_authn_mongo_tls_SUITE.erl
  34. 7 8
      apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl
  35. 7 8
      apps/emqx_authn/test/emqx_authn_mysql_tls_SUITE.erl
  36. 7 9
      apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl
  37. 7 6
      apps/emqx_authn/test/emqx_authn_pgsql_tls_SUITE.erl
  38. 7 8
      apps/emqx_authn/test/emqx_authn_redis_SUITE.erl
  39. 7 9
      apps/emqx_authn/test/emqx_authn_redis_tls_SUITE.erl
  40. 7 6
      apps/emqx_authn/test/emqx_authn_schema_SUITE.erl
  41. 6 5
      apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl
  42. 4 1
      apps/emqx_conf/rebar.config
  43. 1 1
      apps/emqx_conf/src/emqx_conf.app.src
  44. 2 2
      apps/emqx_conf/src/emqx_conf_cli.erl
  45. 6 6
      apps/emqx_conf/src/emqx_conf_schema.erl
  46. 2 1
      apps/emqx_gateway/rebar.config
  47. 1 1
      apps/emqx_gateway/src/emqx_gateway_conf.erl
  48. 1 1
      apps/emqx_gateway/src/emqx_gateway_http.erl
  49. 1 1
      apps/emqx_gateway/src/emqx_gateway_schema.erl
  50. 9 5
      apps/emqx_gcp_device/test/emqx_gcp_device_SUITE.erl
  51. 18 16
      apps/emqx_gcp_device/test/emqx_gcp_device_api_SUITE.erl
  52. 7 5
      apps/emqx_gcp_device/test/emqx_gcp_device_authn_SUITE.erl
  53. 7 7
      apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl
  54. 1 0
      apps/emqx_machine/rebar.config
  55. 27 0
      rel/i18n/emqx_authn_schema.hocon
  56. 0 24
      rel/i18n/emqx_schema.hocon

+ 0 - 16
apps/emqx/include/emqx.hrl

@@ -123,20 +123,4 @@
     until :: integer()
 }).
 
-%%--------------------------------------------------------------------
-%% Authentication
-%%--------------------------------------------------------------------
-
--record(authenticator, {
-    id :: binary(),
-    provider :: module(),
-    enable :: boolean(),
-    state :: map()
-}).
-
--record(chain, {
-    name :: atom(),
-    authenticators :: [#authenticator{}]
-}).
-
 -endif.

+ 7 - 1
apps/emqx/include/emqx_access_control.hrl

@@ -14,7 +14,9 @@
 %% limitations under the License.
 %%--------------------------------------------------------------------
 
-%% config root name all auth providers have to agree on.
+-ifndef(EMQX_ACCESS_CONTROL_HRL).
+-define(EMQX_ACCESS_CONTROL_HRL, true).
+
 -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME, "authorization").
 -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_ATOM, authorization).
 -define(EMQX_AUTHORIZATION_CONFIG_ROOT_NAME_BINARY, <<"authorization">>).
@@ -32,3 +34,7 @@
 -define(authz_action(PUBSUB, QOS), #{action_type := PUBSUB, qos := QOS}).
 -define(authz_action(PUBSUB), ?authz_action(PUBSUB, _)).
 -define(authz_action, ?authz_action(_)).
+
+-define(AUTHN_TRACE_TAG, "AUTHN").
+
+-endif.

+ 33 - 1
apps/emqx/src/emqx_access_control.erl

@@ -17,6 +17,7 @@
 -module(emqx_access_control).
 
 -include("emqx.hrl").
+-include("emqx_access_control.hrl").
 -include("logger.hrl").
 
 -export([
@@ -29,6 +30,14 @@
 -compile(nowarn_export_all).
 -endif.
 
+-define(TRACE_RESULT(Label, Result, Reason), begin
+    ?TRACE(Label, ?AUTHN_TRACE_TAG, #{
+        result => (Result),
+        reason => (Reason)
+    }),
+    Result
+end).
+
 %%--------------------------------------------------------------------
 %% APIs
 %%--------------------------------------------------------------------
@@ -44,7 +53,7 @@ authenticate(Credential) ->
     %% if auth backend returning nothing but just 'ok'
     %% it means it's not a superuser, or there is no way to tell.
     NotSuperUser = #{is_superuser => false},
-    case emqx_authentication:pre_hook_authenticate(Credential) of
+    case pre_hook_authenticate(Credential) of
         ok ->
             inc_authn_metrics(anonymous),
             {ok, NotSuperUser};
@@ -99,6 +108,29 @@ authorize(ClientInfo, Action, Topic) ->
     inc_authz_metrics(Result),
     Result.
 
+%%--------------------------------------------------------------------
+%% Internal Functions
+%%--------------------------------------------------------------------
+
+-spec pre_hook_authenticate(emqx_types:clientinfo()) ->
+    ok | continue | {error, not_authorized}.
+pre_hook_authenticate(#{enable_authn := false}) ->
+    ?TRACE_RESULT("pre_hook_authenticate", ok, enable_authn_false);
+pre_hook_authenticate(#{enable_authn := quick_deny_anonymous} = Credential) ->
+    case is_username_defined(Credential) of
+        true ->
+            continue;
+        false ->
+            ?TRACE_RESULT("pre_hook_authenticate", {error, not_authorized}, enable_authn_false)
+    end;
+pre_hook_authenticate(_) ->
+    continue.
+
+is_username_defined(#{username := undefined}) -> false;
+is_username_defined(#{username := <<>>}) -> false;
+is_username_defined(#{username := _Username}) -> true;
+is_username_defined(_) -> false.
+
 check_authorization_cache(ClientInfo, Action, Topic) ->
     case emqx_authz_cache:get_authz_cache(Action, Topic) of
         not_found ->

+ 1 - 11
apps/emqx/src/emqx_broker_sup.erl

@@ -49,16 +49,6 @@ init([]) ->
         modules => [emqx_shared_sub]
     },
 
-    %% Authentication
-    AuthNSup = #{
-        id => emqx_authentication_sup,
-        start => {emqx_authentication_sup, start_link, []},
-        restart => permanent,
-        shutdown => infinity,
-        type => supervisor,
-        modules => [emqx_authentication_sup]
-    },
-
     %% Broker helper
     Helper = #{
         id => helper,
@@ -69,4 +59,4 @@ init([]) ->
         modules => [emqx_broker_helper]
     },
 
-    {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthNSup, Helper]}}.
+    {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}.

+ 267 - 47
apps/emqx/src/emqx_config_handler.erl

@@ -53,11 +53,17 @@
 
 -optional_callbacks([
     pre_config_update/3,
-    post_config_update/5
+    propagated_pre_config_update/3,
+    post_config_update/5,
+    propagated_post_config_update/5
 ]).
 
 -callback pre_config_update([atom()], emqx_config:update_request(), emqx_config:raw_config()) ->
-    {ok, emqx_config:update_request()} | {error, term()}.
+    ok | {ok, emqx_config:update_request()} | {error, term()}.
+-callback propagated_pre_config_update(
+    [binary()], emqx_config:update_request(), emqx_config:raw_config()
+) ->
+    ok | {ok, emqx_config:update_request()} | {error, term()}.
 
 -callback post_config_update(
     [atom()],
@@ -68,6 +74,15 @@
 ) ->
     ok | {ok, Result :: any()} | {error, Reason :: term()}.
 
+-callback propagated_post_config_update(
+    [atom()],
+    emqx_config:update_request(),
+    emqx_config:config(),
+    emqx_config:config(),
+    emqx_config:app_envs()
+) ->
+    ok | {ok, Result :: any()} | {error, Reason :: term()}.
+
 -type state() :: #{handlers := any()}.
 
 start_link() ->
@@ -244,7 +259,14 @@ do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) ->
     do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq, []).
 
 do_update_config([], Handlers, OldRawConf, UpdateReq, ConfKeyPath) ->
-    call_pre_config_update(Handlers, OldRawConf, UpdateReq, ConfKeyPath);
+    call_pre_config_update(#{
+        handlers => Handlers,
+        old_raw_conf => OldRawConf,
+        update_req => UpdateReq,
+        conf_key_path => ConfKeyPath,
+        callback => pre_config_update,
+        is_propagated => false
+    });
 do_update_config(
     [ConfKey | SubConfKeyPath],
     Handlers,
@@ -331,15 +353,16 @@ do_post_config_update(
     Result,
     ConfKeyPath
 ) ->
-    call_post_config_update(
-        Handlers,
-        OldConf,
-        NewConf,
-        AppEnvs,
-        up_req(UpdateArgs),
-        Result,
-        ConfKeyPath
-    );
+    call_post_config_update(#{
+        handlers => Handlers,
+        old_conf => OldConf,
+        new_conf => NewConf,
+        app_envs => AppEnvs,
+        update_req => up_req(UpdateArgs),
+        result => Result,
+        conf_key_path => ConfKeyPath,
+        callback => post_config_update
+    });
 do_post_config_update(
     [ConfKey | SubConfKeyPath],
     Handlers,
@@ -365,10 +388,16 @@ do_post_config_update(
         ConfKeyPath
     ).
 
-get_sub_handlers(ConfKey, Handlers) ->
+get_sub_handlers(ConfKey, Handlers) when is_atom(ConfKey) ->
     case maps:find(ConfKey, Handlers) of
         error -> maps:get(?WKEY, Handlers, #{});
         {ok, SubHandlers} -> SubHandlers
+    end;
+get_sub_handlers(ConfKey, Handlers) when is_binary(ConfKey) ->
+    ConcreteHandlerKeys = maps:keys(Handlers) -- [?MOD, ?WKEY],
+    case lists:search(fun(K) -> bin(K) =:= ConfKey end, ConcreteHandlerKeys) of
+        {value, Key} -> maps:get(Key, Handlers);
+        false -> maps:get(?WKEY, Handlers, #{})
     end.
 
 get_sub_config(ConfKey, Conf) when is_map(Conf) ->
@@ -377,57 +406,247 @@ get_sub_config(ConfKey, Conf) when is_map(Conf) ->
 get_sub_config(_, _Conf) ->
     undefined.
 
-call_pre_config_update(#{?MOD := HandlerName}, OldRawConf, UpdateReq, ConfKeyPath) ->
-    case erlang:function_exported(HandlerName, pre_config_update, 3) of
+call_pre_config_update(Ctx) ->
+    case call_proper_pre_config_update(Ctx) of
+        {ok, NewUpdateReq0} ->
+            case
+                propagate_pre_config_updates_to_subconf(Ctx#{
+                    update_req => NewUpdateReq0
+                })
+            of
+                {ok, #{update_req := NewUpdateReq1}} ->
+                    {ok, NewUpdateReq1};
+                {error, _} = Error ->
+                    Error
+            end;
+        {error, _} = Error ->
+            Error
+    end.
+
+call_proper_pre_config_update(
+    #{
+        handlers := #{?MOD := Module},
+        callback := Callback,
+        update_req := UpdateReq,
+        old_raw_conf := OldRawConf
+    } = Ctx
+) ->
+    case erlang:function_exported(Module, Callback, 3) of
         true ->
-            case HandlerName:pre_config_update(ConfKeyPath, UpdateReq, OldRawConf) of
-                {ok, NewUpdateReq} -> {ok, NewUpdateReq};
-                {error, Reason} -> {error, {pre_config_update, HandlerName, Reason}}
+            case apply_pre_config_update(Module, Ctx) of
+                {ok, NewUpdateReq} ->
+                    {ok, NewUpdateReq};
+                ok ->
+                    {ok, UpdateReq};
+                {error, Reason} ->
+                    {error, {pre_config_update, Module, Reason}}
             end;
         false ->
             merge_to_old_config(UpdateReq, OldRawConf)
     end;
-call_pre_config_update(_Handlers, OldRawConf, UpdateReq, _ConfKeyPath) ->
-    merge_to_old_config(UpdateReq, OldRawConf).
+call_proper_pre_config_update(
+    #{update_req := UpdateReq}
+) ->
+    {ok, UpdateReq}.
 
-call_post_config_update(
-    #{?MOD := HandlerName},
-    OldConf,
-    NewConf,
-    AppEnvs,
-    UpdateReq,
-    Result,
-    ConfKeyPath
+apply_pre_config_update(Module, #{
+    conf_key_path := ConfKeyPath,
+    update_req := UpdateReq,
+    old_raw_conf := OldRawConf,
+    callback := Callback
+}) ->
+    Module:Callback(
+        ConfKeyPath, UpdateReq, OldRawConf
+    ).
+
+propagate_pre_config_updates_to_subconf(
+    #{handlers := #{?WKEY := _}} = Ctx
 ) ->
-    case erlang:function_exported(HandlerName, post_config_update, 5) of
-        true ->
+    propagate_pre_config_updates_to_subconf_wkey(Ctx);
+propagate_pre_config_updates_to_subconf(
+    #{handlers := Handlers} = Ctx
+) ->
+    Keys = maps:keys(maps:without([?MOD], Handlers)),
+    propagate_pre_config_updates_to_subconf_keys(Keys, Ctx).
+
+propagate_pre_config_updates_to_subconf_wkey(
+    #{
+        update_req := UpdateReq,
+        old_raw_conf := OldRawConf
+    } = Ctx
+) ->
+    Keys = propagate_keys(UpdateReq, OldRawConf),
+    propagate_pre_config_updates_to_subconf_keys(Keys, Ctx).
+
+propagate_pre_config_updates_to_subconf_keys([], Ctx) ->
+    {ok, Ctx};
+propagate_pre_config_updates_to_subconf_keys([Key | Keys], Ctx0) ->
+    case propagate_pre_config_updates_to_subconf_key(Key, Ctx0) of
+        {ok, Ctx1} ->
+            propagate_pre_config_updates_to_subconf_keys(Keys, Ctx1);
+        {error, _} = Error ->
+            Error
+    end.
+
+propagate_pre_config_updates_to_subconf_key(
+    Key,
+    #{
+        handlers := Handlers,
+        old_raw_conf := OldRawConf,
+        update_req := UpdateReq,
+        conf_key_path := ConfKeyPath,
+        is_propagated := IsPropagated
+    } = Ctx
+) ->
+    BinKey = bin(Key),
+    SubHandlers = get_sub_handlers(BinKey, Handlers),
+    SubUpdateReq = get_sub_config(BinKey, UpdateReq),
+    SubOldConf = get_sub_config(BinKey, OldRawConf),
+    SubConfKeyPath =
+        case IsPropagated of
+            true -> ConfKeyPath ++ [BinKey];
+            false -> bin_path(ConfKeyPath) ++ [BinKey]
+        end,
+    case {SubOldConf, SubUpdateReq} of
+        %% we have handler, but no relevant keys in both configs (new and old),
+        %% so we don't need to go further
+        {undefined, undefined} ->
+            {ok, Ctx};
+        {_, _} ->
             case
-                HandlerName:post_config_update(
-                    ConfKeyPath,
-                    UpdateReq,
-                    NewConf,
-                    OldConf,
-                    AppEnvs
-                )
+                call_pre_config_update(Ctx#{
+                    handlers := SubHandlers,
+                    old_raw_conf := SubOldConf,
+                    update_req := SubUpdateReq,
+                    conf_key_path := SubConfKeyPath,
+                    is_propagated := true,
+                    callback := propagated_pre_config_update
+                })
             of
+                {ok, SubNewConf1} ->
+                    %% we update only if the new config is not to be removed
+                    %% i.e. SubUpdateReq is not undefined
+                    case SubUpdateReq of
+                        undefined ->
+                            {ok, Ctx};
+                        _ ->
+                            {ok, Ctx#{
+                                update_req := maps:put(BinKey, SubNewConf1, UpdateReq)
+                            }}
+                    end;
+                {error, _} = Error ->
+                    Error
+            end
+    end.
+
+call_post_config_update(#{handlers := Handlers} = Ctx) ->
+    case call_proper_post_config_update(Ctx) of
+        {ok, Result} ->
+            SubHandlers = maps:without([?MOD], Handlers),
+            propagate_post_config_updates_to_subconf(Ctx#{
+                handlers := SubHandlers,
+                callback := propagated_post_config_update,
+                result := Result
+            });
+        {error, _} = Error ->
+            Error
+    end.
+
+call_proper_post_config_update(
+    #{
+        handlers := #{?MOD := Module},
+        callback := Callback,
+        result := Result
+    } = Ctx
+) ->
+    case erlang:function_exported(Module, Callback, 5) of
+        true ->
+            case apply_post_config_update(Module, Ctx) of
                 ok -> {ok, Result};
-                {ok, Result1} -> {ok, Result#{HandlerName => Result1}};
-                {error, Reason} -> {error, {post_config_update, HandlerName, Reason}}
+                {ok, Result1} -> {ok, Result#{Module => Result1}};
+                {error, Reason} -> {error, {post_config_update, Module, Reason}}
             end;
         false ->
             {ok, Result}
     end;
-call_post_config_update(
-    _Handlers,
-    _OldConf,
-    _NewConf,
-    _AppEnvs,
-    _UpdateReq,
-    Result,
-    _ConfKeyPath
+call_proper_post_config_update(
+    #{result := Result} = _Ctx
 ) ->
     {ok, Result}.
 
+apply_post_config_update(Module, #{
+    conf_key_path := ConfKeyPath,
+    update_req := UpdateReq,
+    new_conf := NewConf,
+    old_conf := OldConf,
+    app_envs := AppEnvs,
+    callback := Callback
+}) ->
+    Module:Callback(
+        ConfKeyPath,
+        UpdateReq,
+        NewConf,
+        OldConf,
+        AppEnvs
+    ).
+
+propagate_post_config_updates_to_subconf(
+    #{handlers := #{?WKEY := _}} = Ctx
+) ->
+    propagate_post_config_updates_to_subconf_wkey(Ctx);
+propagate_post_config_updates_to_subconf(
+    #{handlers := Handlers} = Ctx
+) ->
+    Keys = maps:keys(Handlers),
+    propagate_post_config_updates_to_subconf_keys(Keys, Ctx).
+
+propagate_post_config_updates_to_subconf_wkey(
+    #{
+        old_conf := OldConf,
+        new_conf := NewConf
+    } = Ctx
+) ->
+    Keys = propagate_keys(OldConf, NewConf),
+    propagate_post_config_updates_to_subconf_keys(Keys, Ctx).
+propagate_post_config_updates_to_subconf_keys([], #{result := Result}) ->
+    {ok, Result};
+propagate_post_config_updates_to_subconf_keys([Key | Keys], Ctx) ->
+    case propagate_post_config_updates_to_subconf_key(Key, Ctx) of
+        {ok, Result1} ->
+            propagate_post_config_updates_to_subconf_keys(Keys, Ctx#{result := Result1});
+        Error ->
+            Error
+    end.
+
+propagate_keys(OldConf, NewConf) ->
+    sets:to_list(sets:union(propagate_keys(OldConf), propagate_keys(NewConf))).
+
+propagate_keys(Conf) when is_map(Conf) -> sets:from_list(maps:keys(Conf), [{version, 2}]);
+propagate_keys(_) -> sets:new([{version, 2}]).
+
+propagate_post_config_updates_to_subconf_key(
+    Key,
+    #{
+        handlers := Handlers,
+        new_conf := NewConf,
+        old_conf := OldConf,
+        result := Result,
+        conf_key_path := ConfKeyPath
+    } = Ctx
+) ->
+    SubHandlers = maps:get(Key, Handlers, maps:get(?WKEY, Handlers, undefined)),
+    SubNewConf = get_sub_config(Key, NewConf),
+    SubOldConf = get_sub_config(Key, OldConf),
+    SubConfKeyPath = ConfKeyPath ++ [Key],
+    call_post_config_update(Ctx#{
+        handlers := SubHandlers,
+        new_conf := SubNewConf,
+        old_conf := SubOldConf,
+        result := Result,
+        conf_key_path := SubConfKeyPath,
+        callback := propagated_post_config_update
+    }).
+
 %% The default callback of config handlers
 %% the behaviour is overwriting the old config if:
 %%   1. the old config is undefined
@@ -517,6 +736,7 @@ remove_empty_leaf(KeyPath, Handlers) ->
     end.
 
 assert_callback_function(Mod) ->
+    _ = Mod:module_info(),
     case
         erlang:function_exported(Mod, pre_config_update, 3) orelse
             erlang:function_exported(Mod, post_config_update, 5)

+ 6 - 46
apps/emqx/src/emqx_listeners.erl

@@ -531,41 +531,15 @@ post_config_update(_Path, _Request, _NewConf, _OldConf, _AppEnvs) ->
     ok.
 
 create_listener(Type, Name, NewConf) ->
-    Res = start_listener(Type, Name, NewConf),
-    recreate_authenticators(Res, Type, Name, NewConf).
-
-recreate_authenticators(ok, Type, Name, Conf) ->
-    Chain = listener_id(Type, Name),
-    _ = emqx_authentication:delete_chain(Chain),
-    do_create_authneticators(Chain, maps:get(authentication, Conf, []));
-recreate_authenticators(Error, _Type, _Name, _NewConf) ->
-    Error.
-
-do_create_authneticators(Chain, [AuthN | T]) ->
-    case emqx_authentication:create_authenticator(Chain, AuthN) of
-        {ok, _} ->
-            do_create_authneticators(Chain, T);
-        Error ->
-            _ = emqx_authentication:delete_chain(Chain),
-            Error
-    end;
-do_create_authneticators(_Chain, []) ->
-    ok.
+    start_listener(Type, Name, NewConf).
 
 remove_listener(Type, Name, OldConf) ->
     ok = unregister_ocsp_stapling_refresh(Type, Name),
-    case stop_listener(Type, Name, OldConf) of
-        ok ->
-            _ = emqx_authentication:delete_chain(listener_id(Type, Name)),
-            ok;
-        Err ->
-            Err
-    end.
+    stop_listener(Type, Name, OldConf).
 
 update_listener(Type, Name, {OldConf, NewConf}) ->
     ok = maybe_unregister_ocsp_stapling_refresh(Type, Name, NewConf),
-    Res = restart_listener(Type, Name, {OldConf, NewConf}),
-    recreate_authenticators(Res, Type, Name, NewConf).
+    restart_listener(Type, Name, {OldConf, NewConf}).
 
 perform_listener_changes([]) ->
     ok;
@@ -847,10 +821,9 @@ convert_certs(ListenerConf) ->
         fun(Type, Listeners0, Acc) ->
             Listeners1 =
                 maps:fold(
-                    fun(Name, Conf, Acc1) ->
-                        Conf1 = convert_certs(Type, Name, Conf),
-                        Conf2 = convert_authn_certs(Type, Name, Conf1),
-                        Acc1#{Name => Conf2}
+                    fun(Name, Conf0, Acc1) ->
+                        Conf1 = convert_certs(Type, Name, Conf0),
+                        Acc1#{Name => Conf1}
                     end,
                     #{},
                     Listeners0
@@ -873,19 +846,6 @@ convert_certs(Type, Name, Conf) ->
             throw({bad_ssl_config, Reason})
     end.
 
-convert_authn_certs(Type, Name, #{<<"authentication">> := AuthNList} = Conf) ->
-    ChainName = listener_id(Type, Name),
-    AuthNList1 = lists:map(
-        fun(AuthN) ->
-            CertsDir = emqx_authentication_config:certs_dir(ChainName, AuthN),
-            emqx_authentication_config:convert_certs(CertsDir, AuthN)
-        end,
-        AuthNList
-    ),
-    Conf#{<<"authentication">> => AuthNList1};
-convert_authn_certs(_Type, _Name, Conf) ->
-    Conf.
-
 filter_stacktrace({Reason, _Stacktrace}) -> Reason;
 filter_stacktrace(Reason) -> Reason.
 

+ 13 - 50
apps/emqx/src/emqx_schema.erl

@@ -24,7 +24,6 @@
 -elvis([{elvis_style, invalid_dynamic_call, disable}]).
 
 -include("emqx_schema.hrl").
--include("emqx_authentication.hrl").
 -include("emqx_access_control.hrl").
 -include_lib("typerefl/include/types.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
@@ -213,16 +212,18 @@ roots(high) ->
                     desc => ?DESC(zones),
                     importance => ?IMPORTANCE_HIDDEN
                 }
-            )},
-        {?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME, authentication(global)},
-        %% NOTE: authorization schema here is only to keep emqx app pure
-        %% the full schema for EMQX node is injected in emqx_conf_schema.
-        {?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME,
-            sc(
-                ref(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME),
-                #{importance => ?IMPORTANCE_HIDDEN}
             )}
-    ];
+    ] ++
+        emqx_schema_hooks:injection_point('roots.high') ++
+        [
+            %% NOTE: authorization schema here is only to keep emqx app pure
+            %% the full schema for EMQX node is injected in emqx_conf_schema.
+            {?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME,
+                sc(
+                    ref(?EMQX_AUTHORIZATION_CONFIG_ROOT_NAME),
+                    #{importance => ?IMPORTANCE_HIDDEN}
+                )}
+        ];
 roots(medium) ->
     [
         {"broker",
@@ -1748,11 +1749,8 @@ mqtt_listener(Bind) ->
                         desc => ?DESC(mqtt_listener_proxy_protocol_timeout),
                         default => <<"3s">>
                     }
-                )},
-            {?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME, (authentication(listener))#{
-                importance => ?IMPORTANCE_HIDDEN
-            }}
-        ].
+                )}
+        ] ++ emqx_schema_hooks:injection_point('mqtt.listener').
 
 base_listener(Bind) ->
     [
@@ -2770,41 +2768,6 @@ str(B) when is_binary(B) ->
 str(S) when is_list(S) ->
     S.
 
-authentication(Which) ->
-    {Importance, Desc} =
-        case Which of
-            global ->
-                %% For root level authentication, it is recommended to configure
-                %% from the dashboard or API.
-                %% Hence it's considered a low-importance when it comes to
-                %% configuration importance.
-                {?IMPORTANCE_LOW, ?DESC(global_authentication)};
-            listener ->
-                {?IMPORTANCE_HIDDEN, ?DESC(listener_authentication)}
-        end,
-    %% poor man's dependency injection
-    %% this is due to the fact that authn is implemented outside of 'emqx' app.
-    %% so it can not be a part of emqx_schema since 'emqx' app is supposed to
-    %% work standalone.
-    Type =
-        case persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, undefined) of
-            undefined ->
-                hoconsc:array(typerefl:map());
-            Module ->
-                Module:root_type()
-        end,
-    hoconsc:mk(Type, #{
-        desc => Desc,
-        converter => fun ensure_array/2,
-        default => [],
-        importance => Importance
-    }).
-
-%% the older version schema allows individual element (instead of a chain) in config
-ensure_array(undefined, _) -> undefined;
-ensure_array(L, _) when is_list(L) -> L;
-ensure_array(M, _) -> [M].
-
 -spec qos() -> typerefl:type().
 qos() ->
     typerefl:alias("qos", typerefl:union([0, 1, 2])).

+ 118 - 0
apps/emqx/src/emqx_schema_hooks.erl

@@ -0,0 +1,118 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2017-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_schema_hooks).
+
+-type hookpoint() :: atom().
+
+-callback injected_fields() ->
+    #{
+        hookpoint() => [hocon_schema:field()]
+    }.
+-optional_callbacks([injected_fields/0]).
+
+-export_type([hookpoint/0]).
+
+-define(HOOKPOINT_PT_KEY(POINT_NAME), {?MODULE, fields, POINT_NAME}).
+
+-export([
+    injection_point/1,
+    inject_from_modules/1
+]).
+
+%% for tests
+-export([
+    erase_injections/0,
+    any_injections/0
+]).
+
+%%--------------------------------------------------------------------
+%% API
+%%--------------------------------------------------------------------
+
+injection_point(PointName) ->
+    persistent_term:get(?HOOKPOINT_PT_KEY(PointName), []).
+
+erase_injections() ->
+    lists:foreach(
+        fun
+            ({?HOOKPOINT_PT_KEY(_) = Key, _}) ->
+                persistent_term:erase(Key);
+            (_) ->
+                ok
+        end,
+        persistent_term:get()
+    ).
+
+any_injections() ->
+    lists:any(
+        fun
+            ({?HOOKPOINT_PT_KEY(_), _}) ->
+                true;
+            (_) ->
+                false
+        end,
+        persistent_term:get()
+    ).
+
+inject_from_modules(Modules) ->
+    Injections =
+        lists:foldl(
+            fun append_module_injections/2,
+            #{},
+            Modules
+        ),
+    ok = inject_fields(maps:to_list(Injections)).
+
+%%--------------------------------------------------------------------
+%% Internal functions
+%%--------------------------------------------------------------------
+
+append_module_injections(Module, AllInjections) when is_atom(Module) ->
+    append_module_injections(Module:injected_fields(), AllInjections);
+append_module_injections(ModuleInjections, AllInjections) when is_map(ModuleInjections) ->
+    maps:fold(
+        fun(PointName, Fields, Acc) ->
+            maps:update_with(
+                PointName,
+                fun(Fields0) ->
+                    Fields0 ++ Fields
+                end,
+                Fields,
+                Acc
+            )
+        end,
+        AllInjections,
+        ModuleInjections
+    ).
+
+inject_fields([]) ->
+    ok;
+inject_fields([{PointName, Fields} | Rest]) ->
+    case any_injections(PointName) of
+        true ->
+            inject_fields(Rest);
+        false ->
+            ok = inject_fields(PointName, Fields),
+            inject_fields(Rest)
+    end.
+
+inject_fields(PointName, Fields) ->
+    Key = ?HOOKPOINT_PT_KEY(PointName),
+    persistent_term:put(Key, Fields).
+
+any_injections(PointName) ->
+    persistent_term:get(?HOOKPOINT_PT_KEY(PointName), undefined) =/= undefined.

+ 12 - 19
apps/emqx/test/emqx_broker_SUITE.erl

@@ -26,6 +26,7 @@
 -include_lib("snabbkaffe/include/snabbkaffe.hrl").
 
 -include_lib("emqx/include/emqx.hrl").
+-include_lib("emqx/include/emqx_hooks.hrl").
 -include_lib("emqx/include/emqx_mqtt.hrl").
 
 all() ->
@@ -680,28 +681,17 @@ t_connect_client_never_negative({'end', _Config}) ->
 
 t_connack_auth_error({init, Config}) ->
     process_flag(trap_exit, true),
-    ChainName = 'mqtt:global',
-    AuthenticatorConfig = #{
-        enable => true,
-        mechanism => password_based,
-        backend => built_in_database,
-        user_id_type => username,
-        password_hash_algorithm => #{
-            name => plain,
-            salt_position => disable
-        },
-        user_group => <<"global:mqtt">>
-    },
-    ok = emqx_authentication:register_providers(
-        [{{password_based, built_in_database}, emqx_authentication_SUITE}]
+    emqx_hooks:put(
+        'client.authenticate',
+        {?MODULE, authenticate_deny, []},
+        ?HP_AUTHN
     ),
-    emqx_authentication:initialize_authentication(ChainName, AuthenticatorConfig),
     Config;
 t_connack_auth_error({'end', _Config}) ->
-    ChainName = 'mqtt:global',
-    AuthenticatorID = <<"password_based:built_in_database">>,
-    ok = emqx_authentication:deregister_provider({password_based, built_in_database}),
-    ok = emqx_authentication:delete_authenticator(ChainName, AuthenticatorID),
+    emqx_hooks:del(
+        'client.authenticate',
+        {?MODULE, authenticate_deny, []}
+    ),
     ok;
 t_connack_auth_error(Config) when is_list(Config) ->
     %% MQTT 3.1
@@ -733,6 +723,9 @@ t_handle_in_empty_client_subscribe_hook(Config) when is_list(Config) ->
         emqtt:disconnect(C)
     end.
 
+authenticate_deny(_Credentials, _Default) ->
+    {stop, {error, bad_username_or_password}}.
+
 wait_for_events(Action, Kinds) ->
     wait_for_events(Action, Kinds, 500).
 

+ 1 - 3
apps/emqx/test/emqx_common_test_helpers.erl

@@ -16,8 +16,6 @@
 
 -module(emqx_common_test_helpers).
 
--include_lib("emqx/include/emqx_authentication.hrl").
-
 -type special_config_handler() :: fun().
 
 -type apps() :: list(atom()).
@@ -351,7 +349,7 @@ stop_apps(Apps, Opts) ->
     %% to avoid inter-suite flakiness
     application:unset_env(emqx, config_loader),
     application:unset_env(emqx, boot_modules),
-    persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY),
+    emqx_schema_hooks:erase_injections(),
     case Opts of
         #{erase_all_configs := false} ->
             %% FIXME: this means inter-suite or inter-test dependencies

+ 127 - 19
apps/emqx/test/emqx_config_handler_SUITE.erl

@@ -26,7 +26,8 @@
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("common_test/include/ct.hrl").
 
-all() -> emqx_common_test_helpers:all(?MODULE).
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
     emqx_common_test_helpers:boot_modules(all),
@@ -223,8 +224,8 @@ t_callback_crash(_Config) ->
     ok = emqx_config_handler:remove_handler(CrashPath),
     ok.
 
-t_pre_callback_error(_Config) ->
-    callback_error(
+t_pre_assert_update_result(_Config) ->
+    assert_update_result(
         [sysmon, os, mem_check_interval],
         <<"100s">>,
         {error, {pre_config_update, ?MODULE, pre_config_update_error}}
@@ -232,13 +233,88 @@ t_pre_callback_error(_Config) ->
     ok.
 
 t_post_update_error(_Config) ->
-    callback_error(
+    assert_update_result(
         [sysmon, os, sysmem_high_watermark],
         <<"60%">>,
         {error, {post_config_update, ?MODULE, post_config_update_error}}
     ),
     ok.
 
+t_post_update_propagate_error_wkey(_Config) ->
+    Conf0 = emqx_config:get_raw([sysmon]),
+    Conf1 = emqx_utils_maps:deep_put([<<"os">>, <<"sysmem_high_watermark">>], Conf0, <<"60%">>),
+    assert_update_result(
+        [
+            [sysmon, '?', sysmem_high_watermark],
+            [sysmon]
+        ],
+        [sysmon],
+        Conf1,
+        {error, {post_config_update, ?MODULE, post_config_update_error}}
+    ),
+    ok.
+
+t_post_update_propagate_error_key(_Config) ->
+    Conf0 = emqx_config:get_raw([sysmon]),
+    Conf1 = emqx_utils_maps:deep_put([<<"os">>, <<"sysmem_high_watermark">>], Conf0, <<"60%">>),
+    assert_update_result(
+        [
+            [sysmon, os, sysmem_high_watermark],
+            [sysmon]
+        ],
+        [sysmon],
+        Conf1,
+        {error, {post_config_update, ?MODULE, post_config_update_error}}
+    ),
+    ok.
+
+t_pre_update_propagate_error_wkey(_Config) ->
+    Conf0 = emqx_config:get_raw([sysmon]),
+    Conf1 = emqx_utils_maps:deep_put([<<"os">>, <<"mem_check_interval">>], Conf0, <<"70s">>),
+    assert_update_result(
+        [
+            [sysmon, '?', mem_check_interval],
+            [sysmon]
+        ],
+        [sysmon],
+        Conf1,
+        {error, {pre_config_update, ?MODULE, pre_config_update_error}}
+    ),
+    ok.
+
+t_pre_update_propagate_error_key(_Config) ->
+    Conf0 = emqx_config:get_raw([sysmon]),
+    Conf1 = emqx_utils_maps:deep_put([<<"os">>, <<"mem_check_interval">>], Conf0, <<"70s">>),
+    assert_update_result(
+        [
+            [sysmon, os, mem_check_interval],
+            [sysmon]
+        ],
+        [sysmon],
+        Conf1,
+        {error, {pre_config_update, ?MODULE, pre_config_update_error}}
+    ),
+    ok.
+
+t_pre_update_propagate_key_rewrite(_Config) ->
+    Conf0 = emqx_config:get_raw([sysmon]),
+    Conf1 = emqx_utils_maps:deep_put([<<"os">>, <<"cpu_check_interval">>], Conf0, <<"333s">>),
+    with_update_result(
+        [
+            [sysmon, '?', cpu_check_interval],
+            [sysmon]
+        ],
+        [sysmon],
+        Conf1,
+        fun(_, Result) ->
+            ?assertMatch(
+                {ok, #{config := #{os := #{cpu_check_interval := 444000}}}},
+                Result
+            )
+        end
+    ),
+    ok.
+
 t_handler_root() ->
     %% Don't rely on default emqx_config_handler's merge behaviour.
     RootKey = [],
@@ -295,6 +371,17 @@ pre_config_update([sysmon, os, sysmem_high_watermark], UpdateReq, _RawConf) ->
 pre_config_update([sysmon, os, mem_check_interval], _UpdateReq, _RawConf) ->
     {error, pre_config_update_error}.
 
+propagated_pre_config_update(
+    [<<"sysmon">>, <<"os">>, <<"cpu_check_interval">>], <<"333s">>, _RawConf
+) ->
+    {ok, <<"444s">>};
+propagated_pre_config_update(
+    [<<"sysmon">>, <<"os">>, <<"mem_check_interval">>], _UpdateReq, _RawConf
+) ->
+    {error, pre_config_update_error};
+propagated_pre_config_update(_ConfKeyPath, _UpdateReq, _RawConf) ->
+    ok.
+
 post_config_update([sysmon], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
     {ok, ok};
 post_config_update([sysmon, os], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
@@ -308,6 +395,13 @@ post_config_update([sysmon, os, cpu_high_watermark], _UpdateReq, _NewConf, _OldC
 post_config_update([sysmon, os, sysmem_high_watermark], _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
     {error, post_config_update_error}.
 
+propagated_post_config_update(
+    [sysmon, os, sysmem_high_watermark], _UpdateReq, _NewConf, _OldConf, _AppEnvs
+) ->
+    {error, post_config_update_error};
+propagated_post_config_update(_ConfKeyPath, _UpdateReq, _NewConf, _OldConf, _AppEnvs) ->
+    ok.
+
 wait_for_new_pid() ->
     case erlang:whereis(emqx_config_handler) of
         undefined ->
@@ -317,20 +411,34 @@ wait_for_new_pid() ->
             Pid
     end.
 
-callback_error(FailedPath, Update, ExpectError) ->
+assert_update_result(FailedPath, Update, Expect) ->
+    assert_update_result([FailedPath], FailedPath, Update, Expect).
+
+assert_update_result(Paths, UpdatePath, Update, Expect) ->
+    with_update_result(Paths, UpdatePath, Update, fun(Old, Result) ->
+        case Expect of
+            {error, {post_config_update, ?MODULE, post_config_update_error}} ->
+                ?assertMatch(
+                    {error, {post_config_update, ?MODULE, {post_config_update_error, _}}}, Result
+                );
+            _ ->
+                ?assertEqual(Expect, Result)
+        end,
+        New = emqx:get_raw_config(UpdatePath, undefined),
+        ?assertEqual(Old, New)
+    end).
+
+with_update_result(Paths, UpdatePath, Update, Fun) ->
+    ok = lists:foreach(
+        fun(Path) -> emqx_config_handler:add_handler(Path, ?MODULE) end,
+        Paths
+    ),
     Opts = #{rawconf_with_defaults => true},
-    ok = emqx_config_handler:add_handler(FailedPath, ?MODULE),
-    Old = emqx:get_raw_config(FailedPath, undefined),
-    Error = emqx:update_config(FailedPath, Update, Opts),
-    case ExpectError of
-        {error, {post_config_update, ?MODULE, post_config_update_error}} ->
-            ?assertMatch(
-                {error, {post_config_update, ?MODULE, {post_config_update_error, _}}}, Error
-            );
-        _ ->
-            ?assertEqual(ExpectError, Error)
-    end,
-    New = emqx:get_raw_config(FailedPath, undefined),
-    ?assertEqual(Old, New),
-    ok = emqx_config_handler:remove_handler(FailedPath),
+    Old = emqx:get_raw_config(UpdatePath, undefined),
+    Result = emqx:update_config(UpdatePath, Update, Opts),
+    _ = Fun(Old, Result),
+    ok = lists:foreach(
+        fun(Path) -> emqx_config_handler:remove_handler(Path) end,
+        Paths
+    ),
     ok.

+ 3 - 3
apps/emqx/test/emqx_cth_suite.erl

@@ -17,7 +17,7 @@
 -module(emqx_cth_suite).
 
 -include_lib("common_test/include/ct.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx/include/emqx_access_control.hrl").
 
 -export([start/2]).
 -export([stop/1]).
@@ -360,12 +360,12 @@ stop_apps(Apps) ->
 
 verify_clean_suite_state(#{work_dir := WorkDir}) ->
     {ok, []} = file:list_dir(WorkDir),
-    none = persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, none),
+    false = emqx_schema_hooks:any_injections(),
     [] = emqx_config:get_root_names(),
     ok.
 
 clean_suite_state() ->
-    _ = persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY),
+    _ = emqx_schema_hooks:erase_injections(),
     _ = emqx_config:erase_all(),
     ok.
 

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

@@ -18,8 +18,8 @@
 -define(EMQX_AUTHENTICATION_HRL, true).
 
 -include_lib("emqx/include/logger.hrl").
+-include_lib("emqx/include/emqx_access_control.hrl").
 
--define(AUTHN_TRACE_TAG, "AUTHN").
 -define(GLOBAL, 'mqtt:global').
 
 -define(TRACE_AUTHN_PROVIDER(Msg), ?TRACE_AUTHN_PROVIDER(Msg, #{})).
@@ -36,12 +36,6 @@
 -define(EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM, authentication).
 -define(EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY, <<"authentication">>).
 
-%% key to a persistent term which stores a module name in order to inject
-%% schema module at run-time to keep emqx app's compile time purity.
-%% see emqx_schema.erl for more details
-%% and emqx_conf_schema for an examples
--define(EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, emqx_authentication_schema_module).
-
 %% authentication move cmd
 -define(CMD_MOVE_FRONT, front).
 -define(CMD_MOVE_REAR, rear).

+ 1 - 1
apps/emqx_authn/include/emqx_authn.hrl

@@ -17,7 +17,7 @@
 -ifndef(EMQX_AUTHN_HRL).
 -define(EMQX_AUTHN_HRL, true).
 
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx_authentication.hrl").
 
 -define(APP, emqx_authn).
 

+ 2 - 0
apps/emqx_authn/rebar.config

@@ -34,4 +34,6 @@
 {cover_opts, [verbose]}.
 {cover_export_enabled, true}.
 
+{erl_first_files, ["src/emqx_authentication.erl"]}.
+
 {project_plugins, [erlfmt]}.

+ 14 - 19
apps/emqx/src/emqx_authentication.erl

@@ -22,18 +22,27 @@
 
 -behaviour(gen_server).
 
--include("emqx.hrl").
--include("logger.hrl").
 -include("emqx_authentication.hrl").
+-include_lib("emqx/include/logger.hrl").
 -include_lib("emqx/include/emqx_hooks.hrl").
 -include_lib("stdlib/include/ms_transform.hrl").
 
 -define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
--define(IS_UNDEFINED(X), (X =:= undefined orelse X =:= <<>>)).
+
+-record(authenticator, {
+    id :: binary(),
+    provider :: module(),
+    enable :: boolean(),
+    state :: map()
+}).
+
+-record(chain, {
+    name :: atom(),
+    authenticators :: [#authenticator{}]
+}).
 
 %% The authentication entrypoint.
 -export([
-    pre_hook_authenticate/1,
     authenticate/2
 ]).
 
@@ -220,21 +229,6 @@ when
 %%------------------------------------------------------------------------------
 %% Authenticate
 %%------------------------------------------------------------------------------
--spec pre_hook_authenticate(emqx_types:clientinfo()) ->
-    ok | continue | {error, not_authorized}.
-pre_hook_authenticate(#{enable_authn := false}) ->
-    ?TRACE_RESULT("authentication_result", ok, enable_authn_false);
-pre_hook_authenticate(#{enable_authn := quick_deny_anonymous} = Credential) ->
-    case maps:get(username, Credential, undefined) of
-        U when ?IS_UNDEFINED(U) ->
-            ?TRACE_RESULT(
-                "authentication_result", {error, not_authorized}, enable_authn_false
-            );
-        _ ->
-            continue
-    end;
-pre_hook_authenticate(_) ->
-    continue.
 
 authenticate(#{listener := Listener, protocol := Protocol} = Credential, AuthResult) ->
     case get_authenticators(Listener, global_chain(Protocol)) of
@@ -271,6 +265,7 @@ get_enabled(Authenticators) ->
 %%------------------------------------------------------------------------------
 
 %% @doc Get all registered authentication providers.
+-spec get_providers() -> #{authn_type() => module()}.
 get_providers() ->
     call(get_providers).
 

+ 59 - 21
apps/emqx/src/emqx_authentication_config.erl

@@ -21,7 +21,9 @@
 
 -export([
     pre_config_update/3,
-    post_config_update/5
+    post_config_update/5,
+    propagated_pre_config_update/3,
+    propagated_post_config_update/5
 ]).
 
 -export([
@@ -37,7 +39,7 @@
 
 -export_type([config/0]).
 
--include("logger.hrl").
+-include_lib("emqx/include/logger.hrl").
 -include("emqx_authentication.hrl").
 
 -type parsed_config() :: #{
@@ -65,8 +67,8 @@
 
 -spec pre_config_update(list(atom()), update_request(), emqx_config:raw_config()) ->
     {ok, map() | list()} | {error, term()}.
-pre_config_update(Paths, UpdateReq, OldConfig) ->
-    try do_pre_config_update(Paths, UpdateReq, to_list(OldConfig)) of
+pre_config_update(ConfPath, UpdateReq, OldConfig) ->
+    try do_pre_config_update(ConfPath, UpdateReq, to_list(OldConfig)) of
         {error, Reason} -> {error, Reason};
         {ok, NewConfig} -> {ok, NewConfig}
     catch
@@ -130,31 +132,33 @@ do_pre_config_update(_, {move_authenticator, _ChainName, AuthenticatorID, Positi
                     end
             end
     end;
-do_pre_config_update(Paths, {merge_authenticators, NewConfig}, OldConfig) ->
+do_pre_config_update(ConfPath, {merge_authenticators, NewConfig}, OldConfig) ->
     MergeConfig = merge_authenticators(OldConfig, NewConfig),
-    do_pre_config_update(Paths, MergeConfig, OldConfig);
+    do_pre_config_update(ConfPath, MergeConfig, OldConfig);
 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)
-    ]}.
+do_pre_config_update(ConfPath, NewConfig, _OldConfig) ->
+    convert_certs_for_conf_path(ConfPath, NewConfig).
+
+%% @doc Handle listener config changes made at higher level.
+
+-spec propagated_pre_config_update(list(binary()), update_request(), emqx_config:raw_config()) ->
+    {ok, map() | list()} | {error, term()}.
+propagated_pre_config_update(_, OldConfig, OldConfig) ->
+    {ok, OldConfig};
+propagated_pre_config_update(ConfPath, NewConfig, _OldConfig) ->
+    convert_certs_for_conf_path(ConfPath, NewConfig).
 
 -spec post_config_update(
     list(atom()),
     update_request(),
-    map() | list(),
+    map() | list() | undefined,
     emqx_config:raw_config(),
     emqx_config:app_envs()
 ) ->
     ok | {ok, map()} | {error, term()}.
-post_config_update(Paths, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
-    do_post_config_update(Paths, UpdateReq, to_list(NewConfig), OldConfig, AppEnvs).
+post_config_update(ConfPath, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
+    do_post_config_update(ConfPath, UpdateReq, to_list(NewConfig), OldConfig, AppEnvs).
 
 do_post_config_update(
     _, {create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs
@@ -192,8 +196,8 @@ do_post_config_update(
     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),
+do_post_config_update(ConfPath, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) ->
+    ChainName = chain_name(ConfPath),
     OldConfig = to_list(OldConfig0),
     NewConfig = to_list(NewConfig0),
     OldIds = lists:map(fun authenticator_id/1, OldConfig),
@@ -203,6 +207,20 @@ do_post_config_update(Paths, _UpdateReq, NewConfig0, OldConfig0, _AppEnvs) ->
     ok = emqx_authentication:reorder_authenticator(ChainName, NewIds),
     ok.
 
+%% @doc Handle listener config changes made at higher level.
+
+-spec propagated_post_config_update(
+    list(atom()),
+    update_request(),
+    map() | list() | undefined,
+    emqx_config:raw_config(),
+    emqx_config:app_envs()
+) ->
+    ok.
+propagated_post_config_update(ConfPath, UpdateReq, NewConfig, OldConfig, AppEnvs) ->
+    ok = post_config_update(ConfPath, UpdateReq, NewConfig, OldConfig, AppEnvs),
+    ok.
+
 %% create new authenticators and update existing ones
 create_or_update_authenticators(OldIds, ChainName, NewConfig) ->
     lists:foreach(
@@ -238,6 +256,17 @@ to_list(M) when M =:= #{} -> [];
 to_list(M) when is_map(M) -> [M];
 to_list(L) when is_list(L) -> L.
 
+convert_certs_for_conf_path(ConfPath, NewConfig) ->
+    ChainName = chain_name_for_filepath(ConfPath),
+    CovertedConfs = lists:map(
+        fun(Conf) ->
+            CertsDir = certs_dir(ChainName, Conf),
+            convert_certs(CertsDir, Conf)
+        end,
+        to_list(NewConfig)
+    ),
+    {ok, CovertedConfs}.
+
 convert_certs(CertsDir, NewConfig) ->
     NewSSL = maps:get(<<"ssl">>, NewConfig, undefined),
     case emqx_tls_lib:ensure_ssl_files(CertsDir, NewSSL) of
@@ -331,7 +360,16 @@ dir(ChainName, Config) when is_map(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>>).
+    %% Type, Name atoms exist, so let 'Type:Name' exist too.
+    binary_to_atom(<<(atom_to_binary(Type))/binary, ":", (atom_to_binary(Name))/binary>>).
+
+chain_name_for_filepath(Path) ->
+    do_chain_name_for_filepath([to_bin(Key) || Key <- Path]).
+
+do_chain_name_for_filepath([<<"authentication">>]) ->
+    to_bin(?GLOBAL);
+do_chain_name_for_filepath([<<"listeners">>, Type, Name, <<"authentication">>]) ->
+    <<(to_bin(Type))/binary, ":", (to_bin(Name))/binary>>.
 
 merge_authenticators(OriginConf0, NewConf0) ->
     {OriginConf1, NewConf1} =

apps/emqx/src/emqx_authentication_sup.erl → apps/emqx_authn/src/emqx_authentication_sup.erl


+ 0 - 1
apps/emqx_authn/src/emqx_authn_api.erl

@@ -21,7 +21,6 @@
 -include("emqx_authn.hrl").
 -include_lib("emqx/include/logger.hrl").
 -include_lib("emqx/include/emqx_placeholder.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
 
 -import(hoconsc, [mk/2, ref/1, ref/2]).

+ 2 - 3
apps/emqx_authn/src/emqx_authn_app.erl

@@ -26,7 +26,7 @@
     stop/1
 ]).
 
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx_authentication.hrl").
 
 -dialyzer({nowarn_function, [start/2]}).
 
@@ -35,8 +35,7 @@
 %%------------------------------------------------------------------------------
 
 start(_StartType, _StartArgs) ->
-    %% required by test cases, ensure the injection of
-    %% EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY
+    %% required by test cases, ensure the injection of schema
     _ = emqx_conf_schema:roots(),
     ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity),
     {ok, Sup} = emqx_authn_sup:start_link(),

+ 39 - 4
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -19,6 +19,12 @@
 -elvis([{elvis_style, invalid_dynamic_call, disable}]).
 -include_lib("hocon/include/hoconsc.hrl").
 -include("emqx_authn.hrl").
+-include("emqx_authentication.hrl").
+
+-behaviour(emqx_schema_hooks).
+-export([
+    injected_fields/0
+]).
 
 -export([
     common_fields/0,
@@ -28,13 +34,18 @@
     fields/1,
     authenticator_type/0,
     authenticator_type_without_scram/0,
-    root_type/0,
     mechanism/1,
     backend/1
 ]).
 
 roots() -> [].
 
+injected_fields() ->
+    #{
+        'mqtt.listener' => global_auth_fields(),
+        'roots.high' => mqtt_listener_auth_fields()
+    }.
+
 tags() ->
     [<<"Authentication">>].
 
@@ -121,12 +132,36 @@ try_select_union_member(Module, Value) ->
             Module:refs()
     end.
 
-%% authn is a core functionality however implemented outside of emqx app
-%% in emqx_schema, 'authentication' is a map() type which is to allow
-%% EMQX more pluggable.
 root_type() ->
     hoconsc:array(authenticator_type()).
 
+global_auth_fields() ->
+    [
+        {?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM,
+            hoconsc:mk(root_type(), #{
+                desc => ?DESC(global_authentication),
+                converter => fun ensure_array/2,
+                default => [],
+                importance => ?IMPORTANCE_LOW
+            })}
+    ].
+
+mqtt_listener_auth_fields() ->
+    [
+        {?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM,
+            hoconsc:mk(root_type(), #{
+                desc => ?DESC(listener_authentication),
+                converter => fun ensure_array/2,
+                default => [],
+                importance => ?IMPORTANCE_HIDDEN
+            })}
+    ].
+
+%% the older version schema allows individual element (instead of a chain) in config
+ensure_array(undefined, _) -> undefined;
+ensure_array(L, _) when is_list(L) -> L;
+ensure_array(M, _) -> [M].
+
 mechanism(Name) ->
     ?HOCON(
         Name,

+ 11 - 1
apps/emqx_authn/src/emqx_authn_sup.erl

@@ -27,5 +27,15 @@ start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
 init([]) ->
-    ChildSpecs = [],
+    AuthNSup = #{
+        id => emqx_authentication_sup,
+        start => {emqx_authentication_sup, start_link, []},
+        restart => permanent,
+        shutdown => infinity,
+        type => supervisor,
+        modules => [emqx_authentication_sup]
+    },
+
+    ChildSpecs = [AuthNSup],
+
     {ok, {{one_for_one, 10, 10}, ChildSpecs}}.

+ 0 - 1
apps/emqx_authn/src/emqx_authn_user_import_api.erl

@@ -20,7 +20,6 @@
 
 -include("emqx_authn.hrl").
 -include_lib("emqx/include/logger.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
 
 -import(emqx_dashboard_swagger, [error_codes/2]).

+ 30 - 17
apps/emqx/test/emqx_authentication_SUITE.erl

@@ -94,19 +94,19 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    LogLevel = emqx_logger:get_primary_log_level(),
-    ok = emqx_logger:set_log_level(debug),
-    application:set_env(ekka, strict_mode, true),
-    emqx_config:erase_all(),
-    emqx_common_test_helpers:stop_apps([]),
-    emqx_common_test_helpers:boot_modules(all),
-    emqx_common_test_helpers:start_apps([]),
-    [{log_level, LogLevel} | Config].
+    Apps = emqx_cth_suite:start(
+        [
+            emqx,
+            emqx_conf,
+            emqx_authn
+        ],
+        #{work_dir => ?config(priv_dir)}
+    ),
+    ok = deregister_providers(),
+    [{apps, Apps} | Config].
 
 end_per_suite(Config) ->
-    emqx_common_test_helpers:stop_apps([]),
-    LogLevel = ?config(log_level),
-    emqx_logger:set_log_level(LogLevel),
+    emqx_cth_suite:stop(?config(apps)),
     ok.
 
 init_per_testcase(Case, Config) ->
@@ -302,15 +302,20 @@ t_update_config(Config) when is_list(Config) ->
     ok = register_provider(?config("auth1"), ?MODULE),
     ok = register_provider(?config("auth2"), ?MODULE),
     Global = ?config(global),
+    %% We mocked provider implementation, but did't mock the schema
+    %% so we should provide full config
     AuthenticatorConfig1 = #{
-        mechanism => password_based,
-        backend => built_in_database,
-        enable => true
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"built_in_database">>,
+        <<"enable">> => true
     },
     AuthenticatorConfig2 = #{
-        mechanism => password_based,
-        backend => mysql,
-        enable => true
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"mysql">>,
+        <<"query">> => <<"SELECT password_hash, salt FROM users WHERE username = ?">>,
+        <<"server">> => <<"127.0.0.1:5432">>,
+        <<"database">> => <<"emqx">>,
+        <<"enable">> => true
     },
     ID1 = <<"password_based:built_in_database">>,
     ID2 = <<"password_based:mysql">>,
@@ -580,3 +585,11 @@ certs(Certs) ->
 
 register_provider(Type, Module) ->
     ok = ?AUTHN:register_providers([{Type, Module}]).
+
+deregister_providers() ->
+    lists:foreach(
+        fun({Type, _Module}) ->
+            ok = ?AUTHN:deregister_provider(Type)
+        end,
+        maps:to_list(?AUTHN:get_providers())
+    ).

+ 17 - 9
apps/emqx_authn/test/emqx_authn_api_SUITE.erl

@@ -23,6 +23,7 @@
 
 -include("emqx_authn.hrl").
 -include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
 
 -define(TCP_DEFAULT, 'tcp:default').
 
@@ -43,7 +44,6 @@ init_per_testcase(t_authenticator_fail, Config) ->
     meck:expect(emqx_authn_proto_v1, lookup_from_all_nodes, 3, [{error, {exception, badarg}}]),
     init_per_testcase(default, Config);
 init_per_testcase(_Case, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authn_test_lib:delete_authenticators(
         [?CONF_NS_ATOM],
         ?GLOBAL
@@ -64,19 +64,27 @@ end_per_testcase(_, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    emqx_config:erase(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY),
-    _ = application:load(emqx_conf),
-    ok = emqx_mgmt_api_test_util:init_suite(
-        [emqx_conf, emqx_authn]
+    Apps = emqx_cth_suite:start(
+        [
+            emqx,
+            emqx_conf,
+            emqx_authn,
+            emqx_management,
+            {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}
+        ],
+        #{
+            work_dir => ?config(priv_dir, Config)
+        }
     ),
-
+    _ = emqx_common_test_http:create_default_app(),
     ?AUTHN:delete_chain(?GLOBAL),
     {ok, Chains} = ?AUTHN:list_chains(),
     ?assertEqual(length(Chains), 0),
-    Config.
+    [{apps, Apps} | Config].
 
-end_per_suite(_Config) ->
-    emqx_mgmt_api_test_util:end_suite([emqx_authn]),
+end_per_suite(Config) ->
+    _ = emqx_common_test_http:delete_default_app(),
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 %%------------------------------------------------------------------------------

+ 10 - 6
apps/emqx_authn/test/emqx_authn_enable_flag_SUITE.erl

@@ -24,16 +24,19 @@
 -define(PATH, [?CONF_NS_ATOM]).
 
 -include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
 
 all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]),
-    Config.
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
 
-end_per_suite(_) ->
-    emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]),
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_Case, Config) ->
@@ -42,9 +45,10 @@ init_per_testcase(_Case, Config) ->
         <<"backend">> => <<"built_in_database">>,
         <<"user_id_type">> => <<"clientid">>
     },
-    {ok, _} = emqx:update_config(
+    {ok, _} = emqx_conf:update(
         ?PATH,
-        {create_authenticator, ?GLOBAL, AuthnConfig}
+        {create_authenticator, ?GLOBAL, AuthnConfig},
+        #{}
     ),
     {ok, _} = emqx_conf:update(
         [listeners, tcp, listener_authn_enabled],

+ 6 - 7
apps/emqx_authn/test/emqx_authn_http_SUITE.erl

@@ -65,18 +65,17 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    emqx_common_test_helpers:start_apps([emqx_authn]),
-    application:ensure_all_started(cowboy),
-    Config.
+    Apps = emqx_cth_suite:start([cowboy, emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
 
-end_per_suite(_) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    emqx_common_test_helpers:stop_apps([emqx_authn]),
-    application:stop(cowboy),
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_Case, Config) ->

+ 6 - 7
apps/emqx_authn/test/emqx_authn_https_SUITE.erl

@@ -39,18 +39,17 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    emqx_common_test_helpers:start_apps([emqx_authn]),
-    application:ensure_all_started(cowboy),
-    Config.
+    Apps = emqx_cth_suite:start([cowboy, emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
 
-end_per_suite(_) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    emqx_common_test_helpers:stop_apps([emqx_authn]),
-    application:stop(cowboy),
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_Case, Config) ->

+ 7 - 14
apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl

@@ -31,21 +31,14 @@
 all() ->
     emqx_common_test_helpers:all(?MODULE).
 
-init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
-    Config.
-
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]),
-    application:ensure_all_started(emqx_resource),
-    application:ensure_all_started(emqx_connector),
-    Config.
-
-end_per_suite(_) ->
-    application:stop(emqx_connector),
-    application:stop(emqx_resource),
-    emqx_common_test_helpers:stop_apps([emqx_authn]),
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
+
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 %%------------------------------------------------------------------------------

+ 242 - 0
apps/emqx_authn/test/emqx_authn_listeners_SUITE.erl

@@ -0,0 +1,242 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_authn_listeners_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include("emqx_authn.hrl").
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
+
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    Port = emqx_common_test_helpers:select_free_port(tcp),
+    [{port, Port} | Config].
+
+end_per_testcase(_Case, _Config) ->
+    ok.
+
+t_create_update_delete(Config) ->
+    ListenerConf = listener_mqtt_tcp_conf(Config),
+    AuthnConfig0 = #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"backend">> => <<"built_in_database">>,
+        <<"user_id_type">> => <<"clientid">>
+    },
+    %% Create
+    {ok, _} = emqx_conf:update(
+        [listeners],
+        #{
+            <<"tcp">> => #{
+                <<"listener0">> => ListenerConf#{
+                    ?CONF_NS_BINARY => AuthnConfig0
+                }
+            }
+        },
+        #{}
+    ),
+    ?assertMatch(
+        {ok, [
+            #{
+                authenticators := [
+                    #{
+                        id := <<"password_based:built_in_database">>,
+                        state := #{
+                            user_id_type := clientid
+                        }
+                    }
+                ],
+                name := 'tcp:listener0'
+            }
+        ]},
+        emqx_authentication:list_chains()
+    ),
+
+    %% Drop old, create new
+    {ok, _} = emqx_conf:update(
+        [listeners],
+        #{
+            <<"tcp">> => #{
+                <<"listener1">> => ListenerConf#{
+                    ?CONF_NS_BINARY => AuthnConfig0
+                }
+            }
+        },
+        #{}
+    ),
+    ?assertMatch(
+        {ok, [
+            #{
+                authenticators := [
+                    #{
+                        id := <<"password_based:built_in_database">>,
+                        state := #{
+                            user_id_type := clientid
+                        }
+                    }
+                ],
+                name := 'tcp:listener1'
+            }
+        ]},
+        emqx_authentication:list_chains()
+    ),
+
+    %% Update
+    {ok, _} = emqx_conf:update(
+        [listeners],
+        #{
+            <<"tcp">> => #{
+                <<"listener1">> => ListenerConf#{
+                    ?CONF_NS_BINARY => AuthnConfig0#{<<"user_id_type">> => <<"username">>}
+                }
+            }
+        },
+        #{}
+    ),
+    ?assertMatch(
+        {ok, [
+            #{
+                authenticators := [
+                    #{
+                        id := <<"password_based:built_in_database">>,
+                        state := #{
+                            user_id_type := username
+                        }
+                    }
+                ],
+                name := 'tcp:listener1'
+            }
+        ]},
+        emqx_authentication:list_chains()
+    ),
+
+    %% Update by listener path
+    {ok, _} = emqx_conf:update(
+        [listeners, tcp, listener1],
+        {update, ListenerConf#{
+            ?CONF_NS_BINARY => AuthnConfig0#{<<"user_id_type">> => <<"clientid">>}
+        }},
+        #{}
+    ),
+    ?assertMatch(
+        {ok, [
+            #{
+                authenticators := [
+                    #{
+                        id := <<"password_based:built_in_database">>,
+                        state := #{
+                            user_id_type := clientid
+                        }
+                    }
+                ],
+                name := 'tcp:listener1'
+            }
+        ]},
+        emqx_authentication:list_chains()
+    ),
+
+    %% Delete
+    {ok, _} = emqx_conf:tombstone(
+        [listeners, tcp, listener1],
+        #{}
+    ),
+    ?assertMatch(
+        {ok, []},
+        emqx_authentication:list_chains()
+    ).
+
+t_convert_certs(Config) ->
+    ListenerConf = listener_mqtt_tcp_conf(Config),
+    AuthnConfig0 = #{
+        <<"mechanism">> => <<"password_based">>,
+        <<"password_hash_algorithm">> => #{
+            <<"name">> => <<"plain">>,
+            <<"salt_position">> => <<"suffix">>
+        },
+        <<"enable">> => <<"true">>,
+
+        <<"backend">> => <<"redis">>,
+        <<"cmd">> => <<"HMGET mqtt_user:${username} password_hash salt is_superuser">>,
+        <<"database">> => <<"1">>,
+        <<"password">> => <<"public">>,
+        <<"server">> => <<"127.0.0.1:55555">>,
+        <<"redis_type">> => <<"single">>,
+        <<"ssl">> => #{
+            <<"enable">> => true,
+            <<"cacertfile">> => some_pem(),
+            <<"certfile">> => some_pem(),
+            <<"keyfile">> => some_pem()
+        }
+    },
+    {ok, _} = emqx_conf:update(
+        [listeners],
+        #{
+            <<"tcp">> => #{
+                <<"listener0">> => ListenerConf#{
+                    ?CONF_NS_BINARY => AuthnConfig0
+                }
+            }
+        },
+        #{}
+    ),
+    lists:foreach(
+        fun(Key) ->
+            [#{ssl := #{Key := FilePath}}] = emqx_config:get([
+                listeners, tcp, listener0, authentication
+            ]),
+            ?assert(filelib:is_regular(FilePath))
+        end,
+        [cacertfile, certfile, keyfile]
+    ).
+
+%%--------------------------------------------------------------------
+%% Helper Functions
+%%--------------------------------------------------------------------
+
+listener_mqtt_tcp_conf(Config) ->
+    Port = ?config(port, Config),
+    PortS = integer_to_binary(Port),
+    #{
+        <<"acceptors">> => 16,
+        <<"access_rules">> => [<<"allow all">>],
+        <<"bind">> => <<"0.0.0.0:", PortS/binary>>,
+        <<"max_connections">> => 1024000,
+        <<"mountpoint">> => <<>>,
+        <<"proxy_protocol">> => false,
+        <<"proxy_protocol_timeout">> => <<"3s">>,
+        <<"enable_authn">> => true
+    }.
+
+some_pem() ->
+    Dir = code:lib_dir(emqx_authn, test),
+    Path = filename:join([Dir, "data", "private_key.pem"]),
+    {ok, Pem} = file:read_file(Path),
+    Pem.

+ 7 - 8
apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl

@@ -20,8 +20,7 @@
 -compile(nowarn_export_all).
 
 -include_lib("eunit/include/eunit.hrl").
-
--include("emqx_authn.hrl").
+-include_lib("common_test/include/ct.hrl").
 
 -define(AUTHN_ID, <<"mechanism:backend">>).
 
@@ -29,16 +28,16 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    emqx_common_test_helpers:start_apps([emqx_authn]),
-    Config.
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
 
-end_per_suite(_) ->
-    emqx_common_test_helpers:stop_apps([emqx_authn]),
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_Case, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     mria:clear_table(emqx_authn_mnesia),
     Config.
 

+ 7 - 8
apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl

@@ -33,7 +33,6 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_testcase(_TestCase, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -46,23 +45,23 @@ end_per_testcase(_TestCase, _Config) ->
     ok = mc_worker_api:disconnect(?MONGO_CLIENT).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
-            Config;
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
+            [{apps, Apps} | Config];
         false ->
             {skip, no_mongo}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 8
apps/emqx_authn/test/emqx_authn_mongo_tls_SUITE.erl

@@ -33,7 +33,6 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_testcase(_TestCase, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -42,23 +41,23 @@ init_per_testcase(_TestCase, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
-            Config;
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
+            [{apps, Apps} | Config];
         false ->
             {skip, no_mongo}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 8
apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl

@@ -37,7 +37,6 @@ groups() ->
     [{require_seeds, [], [t_authenticate, t_update, t_destroy]}].
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -54,11 +53,11 @@ end_per_group(require_seeds, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
             {ok, _} = emqx_resource:create_local(
                 ?MYSQL_RESOURCE,
                 ?RESOURCE_GROUP,
@@ -66,19 +65,19 @@ init_per_suite(Config) ->
                 mysql_config(),
                 #{}
             ),
-            Config;
+            [{apps, Apps} | Config];
         false ->
             {skip, no_mysql}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
     ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 8
apps/emqx_authn/test/emqx_authn_mysql_tls_SUITE.erl

@@ -36,7 +36,6 @@ groups() ->
     [].
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -45,23 +44,23 @@ init_per_testcase(_, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
-            Config;
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
+            [{apps, Apps} | Config];
         false ->
             {skip, no_mysql_tls}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 9
apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl

@@ -23,7 +23,6 @@
 -include_lib("emqx_authn/include/emqx_authn.hrl").
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("common_test/include/ct.hrl").
--include_lib("emqx/include/emqx_placeholder.hrl").
 
 -define(PGSQL_HOST, "pgsql").
 -define(PGSQL_RESOURCE, <<"emqx_authn_pgsql_SUITE">>).
@@ -42,7 +41,6 @@ groups() ->
     [{require_seeds, [], [t_create, t_authenticate, t_update, t_destroy, t_is_superuser]}].
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -59,11 +57,11 @@ end_per_group(require_seeds, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
             {ok, _} = emqx_resource:create_local(
                 ?PGSQL_RESOURCE,
                 ?RESOURCE_GROUP,
@@ -71,19 +69,19 @@ init_per_suite(Config) ->
                 pgsql_config(),
                 #{}
             ),
-            Config;
+            [{apps, Apps} | Config];
         false ->
             {skip, no_pgsql}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
     ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 6
apps/emqx_authn/test/emqx_authn_pgsql_tls_SUITE.erl

@@ -48,20 +48,21 @@ init_per_suite(Config) ->
     _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
-            Config;
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
+            [{apps, Apps} | Config];
         false ->
             {skip, no_pgsql_tls}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 8
apps/emqx_authn/test/emqx_authn_redis_SUITE.erl

@@ -42,7 +42,6 @@ groups() ->
     [{require_seeds, [], [t_authenticate, t_update, t_destroy]}].
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -59,11 +58,11 @@ end_per_group(require_seeds, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
             {ok, _} = emqx_resource:create_local(
                 ?REDIS_RESOURCE,
                 ?RESOURCE_GROUP,
@@ -71,19 +70,19 @@ init_per_suite(Config) ->
                 redis_config(),
                 #{}
             ),
-            Config;
+            [{apps, Apps} | Config];
         false ->
             {skip, no_redis}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
     ok = emqx_resource:remove_local(?REDIS_RESOURCE),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 9
apps/emqx_authn/test/emqx_authn_redis_tls_SUITE.erl

@@ -19,7 +19,6 @@
 -compile(nowarn_export_all).
 -compile(export_all).
 
--include_lib("emqx_connector/include/emqx_connector.hrl").
 -include_lib("emqx_authn/include/emqx_authn.hrl").
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("common_test/include/ct.hrl").
@@ -36,7 +35,6 @@ groups() ->
     [].
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -45,23 +43,23 @@ init_per_testcase(_, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_TLS_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
-            Config;
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
+            [{apps, Apps} | Config];
         false ->
             {skip, no_redis}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 7 - 6
apps/emqx_authn/test/emqx_authn_schema_SUITE.erl

@@ -4,6 +4,7 @@
 -compile(nowarn_export_all).
 
 -include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
 
 -include("emqx_authn.hrl").
 
@@ -11,16 +12,16 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    emqx_common_test_helpers:start_apps([emqx_authn]),
-    Config.
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    [{apps, Apps} | Config].
 
-end_per_suite(_) ->
-    emqx_common_test_helpers:stop_apps([emqx_authn]),
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_Case, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     mria:clear_table(emqx_authn_mnesia),
     Config.
 

+ 6 - 5
apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl

@@ -36,17 +36,18 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    _ = application:load(emqx_conf),
-    ok = emqx_common_test_helpers:start_apps([emqx_authn]),
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
     IdleTimeout = emqx_config:get([mqtt, idle_timeout]),
-    [{idle_timeout, IdleTimeout} | Config].
+    [{apps, Apps}, {idle_timeout, IdleTimeout} | Config].
 
 end_per_suite(Config) ->
     ok = emqx_config:put([mqtt, idle_timeout], ?config(idle_timeout, Config)),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 init_per_testcase(_Case, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     mria:clear_table(emqx_enhanced_authn_scram_mnesia),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],

+ 4 - 1
apps/emqx_conf/rebar.config

@@ -1,7 +1,10 @@
 %% -*- mode: erlang -*-
 
 {erl_opts, [debug_info]}.
-{deps, [{emqx, {path, "../emqx"}}]}.
+{deps, [
+    {emqx, {path, "../emqx"}},
+    {emqx_authn, {path, "../emqx_authn"}}
+]}.
 
 {shell, [
     % {config, "config/sys.config"},

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

@@ -1,6 +1,6 @@
 {application, emqx_conf, [
     {description, "EMQX configuration management"},
-    {vsn, "0.1.26"},
+    {vsn, "0.1.27"},
     {registered, []},
     {mod, {emqx_conf_app, []}},
     {applications, [kernel, stdlib, emqx_ctl]},

+ 2 - 2
apps/emqx_conf/src/emqx_conf_cli.erl

@@ -16,8 +16,8 @@
 
 -module(emqx_conf_cli).
 -include("emqx_conf.hrl").
--include_lib("emqx/include/emqx_access_control.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx_authn/include/emqx_authentication.hrl").
+-include_lib("emqx/include/logger.hrl").
 
 -export([
     load/0,

+ 6 - 6
apps/emqx_conf/src/emqx_conf_schema.erl

@@ -22,9 +22,9 @@
 -dialyzer(no_unused).
 -dialyzer(no_fail_call).
 
+-include_lib("emqx/include/emqx_access_control.hrl").
 -include_lib("typerefl/include/types.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
 
 -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all.
 -type file() :: string().
@@ -66,6 +66,10 @@
     emqx_otel_schema,
     emqx_mgmt_api_key_schema
 ]).
+-define(INJECTING_CONFIGS, [
+    emqx_authn_schema
+]).
+
 %% 1 million default ports counter
 -define(DEFAULT_MAX_PORTS, 1024 * 1024).
 
@@ -76,11 +80,7 @@ tags() ->
     [<<"EMQX">>].
 
 roots() ->
-    PtKey = ?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY,
-    case persistent_term:get(PtKey, undefined) of
-        undefined -> persistent_term:put(PtKey, emqx_authn_schema);
-        _ -> ok
-    end,
+    ok = emqx_schema_hooks:inject_from_modules(?INJECTING_CONFIGS),
     emqx_schema_high_prio_roots() ++
         [
             {"node",

+ 2 - 1
apps/emqx_gateway/rebar.config

@@ -2,5 +2,6 @@
 {erl_opts, [debug_info]}.
 {deps, [
     {emqx, {path, "../emqx"}},
-    {emqx_utils, {path, "../emqx_utils"}}
+    {emqx_utils, {path, "../emqx_utils"}},
+    {emqx_authn, {path, "../emqx_authn"}}
 ]}.

+ 1 - 1
apps/emqx_gateway/src/emqx_gateway_conf.erl

@@ -71,7 +71,7 @@
 ]).
 
 -include_lib("emqx/include/logger.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx_authn/include/emqx_authentication.hrl").
 -define(AUTHN_BIN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY).
 
 -type atom_or_bin() :: atom() | binary().

+ 1 - 1
apps/emqx_gateway/src/emqx_gateway_http.erl

@@ -19,7 +19,7 @@
 
 -include("include/emqx_gateway.hrl").
 -include_lib("emqx/include/logger.hrl").
--include_lib("emqx/include/emqx_authentication.hrl").
+-include_lib("emqx_authn/include/emqx_authentication.hrl").
 
 -define(AUTHN, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
 

+ 1 - 1
apps/emqx_gateway/src/emqx_gateway_schema.erl

@@ -24,9 +24,9 @@
 -dialyzer(no_unused).
 -dialyzer(no_fail_call).
 
--include_lib("emqx/include/emqx_authentication.hrl").
 -include_lib("hocon/include/hoconsc.hrl").
 -include_lib("typerefl/include/types.hrl").
+-include_lib("emqx_authn/include/emqx_authentication.hrl").
 
 -type ip_port() :: tuple() | integer().
 -type duration() :: non_neg_integer().

+ 9 - 5
apps/emqx_gcp_device/test/emqx_gcp_device_SUITE.erl

@@ -17,15 +17,19 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    ok = emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn, emqx_retainer, emqx_gcp_device]),
-    Config.
+    Apps = emqx_cth_suite:start(
+        [emqx, emqx_conf, emqx_authn, emqx_gcp_device, {emqx_retainer, "retainer {enable = true}"}],
+        #{
+            work_dir => ?config(priv_dir, Config)
+        }
+    ),
+    [{apps, Apps} | Config].
 
 end_per_suite(Config) ->
-    _ = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_retainer, emqx_gcp_device]),
-    Config.
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
 
 init_per_testcase(_TestCase, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL

+ 18 - 16
apps/emqx_gcp_device/test/emqx_gcp_device_api_SUITE.erl

@@ -14,32 +14,34 @@
 -include_lib("emqx/include/emqx.hrl").
 
 -define(PATH, [authentication]).
--define(BASE_CONF, <<
-    ""
-    "\n"
-    "retainer {\n"
-    "    enable = true\n"
-    "}"
-    ""
->>).
 
 all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
-    ok = emqx_config:init_load(emqx_retainer_schema, ?BASE_CONF),
-    ok = emqx_common_test_helpers:start_apps([emqx_gcp_device, emqx_authn, emqx_conf, emqx_retainer]),
-    emqx_dashboard_api_test_helpers:set_default_config(),
-    emqx_mgmt_api_test_util:init_suite(),
-    Config.
+    Apps = emqx_cth_suite:start(
+        [
+            emqx,
+            emqx_conf,
+            emqx_authn,
+            {emqx_retainer, "retainer {enable = true}"},
+            emqx_management,
+            {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"},
+            emqx_gcp_device
+        ],
+        #{
+            work_dir => ?config(priv_dir, Config)
+        }
+    ),
+    _ = emqx_common_test_http:create_default_app(),
+    [{apps, Apps} | Config].
 
 end_per_suite(Config) ->
-    emqx_mgmt_api_test_util:end_suite(),
-    _ = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_retainer, emqx_gcp_device]),
+    _ = emqx_common_test_http:delete_default_app(),
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     Config.
 
 init_per_testcase(_TestCase, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL

+ 7 - 5
apps/emqx_gcp_device/test/emqx_gcp_device_authn_SUITE.erl

@@ -23,7 +23,9 @@ all() ->
 
 init_per_suite(Config0) ->
     ok = snabbkaffe:start_trace(),
-    emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn, emqx_gcp_device]),
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn, emqx_gcp_device], #{
+        work_dir => ?config(priv_dir, Config0)
+    }),
     ValidExpirationTime = erlang:system_time(second) + 3600,
     ValidJWT = generate_jws(ValidExpirationTime),
     ExpiredJWT = generate_jws(0),
@@ -35,16 +37,16 @@ init_per_suite(Config0) ->
         {valid_jwt, ValidJWT},
         {expired_jwt, ExpiredJWT},
         {valid_client, ValidClient},
-        {expired_client, ExpiredClient}
+        {expired_client, ExpiredClient},
+        {apps, Apps}
         | Config0
     ].
 
-end_per_suite(_) ->
-    _ = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_gcp_device]),
+end_per_suite(Config) ->
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
     ok.
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     Config.
 
 end_per_testcase(_Case, Config) ->

+ 7 - 7
apps/emqx_ldap/test/emqx_ldap_authn_SUITE.erl

@@ -8,6 +8,7 @@
 
 -include_lib("emqx_authn/include/emqx_authn.hrl").
 -include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
 
 -define(LDAP_HOST, "ldap").
 -define(LDAP_DEFAULT_PORT, 389).
@@ -20,7 +21,6 @@ all() ->
     emqx_common_test_helpers:all(?MODULE).
 
 init_per_testcase(_, Config) ->
-    {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
     emqx_authentication:initialize_authentication(?GLOBAL, []),
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
@@ -32,8 +32,9 @@ init_per_suite(Config) ->
     _ = application:load(emqx_conf),
     case emqx_common_test_helpers:is_tcp_server_available(?LDAP_HOST, ?LDAP_DEFAULT_PORT) of
         true ->
-            ok = emqx_common_test_helpers:start_apps([emqx_authn]),
-            ok = start_apps([emqx_resource]),
+            Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_authn], #{
+                work_dir => ?config(priv_dir, Config)
+            }),
             {ok, _} = emqx_resource:create_local(
                 ?LDAP_RESOURCE,
                 ?RESOURCE_GROUP,
@@ -41,19 +42,18 @@ init_per_suite(Config) ->
                 ldap_config(),
                 #{}
             ),
-            Config;
+            [{apps, Apps} | Config];
         false ->
             {skip, no_ldap}
     end.
 
-end_per_suite(_Config) ->
+end_per_suite(Config) ->
     emqx_authn_test_lib:delete_authenticators(
         [authentication],
         ?GLOBAL
     ),
     ok = emqx_resource:remove_local(?LDAP_RESOURCE),
-    ok = stop_apps([emqx_resource]),
-    ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
+    ok = emqx_cth_suite:stop(?config(apps, Config)).
 
 %%------------------------------------------------------------------------------
 %% Tests

+ 1 - 0
apps/emqx_machine/rebar.config

@@ -3,6 +3,7 @@
 {deps, [
     {emqx, {path, "../emqx"}},
     {emqx_dashboard, {path, "../emqx_dashboard"}},
+    {emqx_conf, {path, "../emqx_conf"}},
     {emqx_utils, {path, "../emqx_utils"}}
 ]}.
 

+ 27 - 0
rel/i18n/emqx_authn_schema.hocon

@@ -1,5 +1,32 @@
 emqx_authn_schema {
 
+global_authentication.desc:
+"""Default authentication configs for all MQTT listeners.
+
+For per-listener overrides see <code>authentication</code> in listener configs
+
+This option can be configured with:
+<ul>
+  <li><code>[]</code>: The default value, it allows *ALL* logins</li>
+  <li>one: For example <code>{enable:true,backend:"built_in_database",mechanism="password_based"}</code></li>
+  <li>chain: An array of structs.</li>
+</ul>
+
+When a chain is configured, the login credentials are checked against the backends per the configured order, until an 'allow' or 'deny' decision can be made.
+
+If there is no decision after a full chain exhaustion, the login is rejected."""
+
+global_authentication.label:
+"""Global authentication"""
+
+listener_authentication.desc:
+"""Per-listener authentication override.
+Authentication can be one single authenticator instance or a chain of authenticators as an array.
+When authenticating a login (username, client ID, etc.) the authenticators are checked in the configured order."""
+
+listener_authentication.label:
+"""Per-listener authentication override"""
+
 backend.desc:
 """Backend type."""
 

+ 0 - 24
rel/i18n/emqx_schema.hocon

@@ -532,22 +532,6 @@ mqtt_server_keepalive.desc:
 mqtt_server_keepalive.label:
 """Server Keep Alive"""
 
-global_authentication.desc:
-"""Default authentication configs for all MQTT listeners.
-
-For per-listener overrides see <code>authentication</code> in listener configs
-
-This option can be configured with:
-<ul>
-  <li><code>[]</code>: The default value, it allows *ALL* logins</li>
-  <li>one: For example <code>{enable:true,backend:"built_in_database",mechanism="password_based"}</code></li>
-  <li>chain: An array of structs.</li>
-</ul>
-
-When a chain is configured, the login credentials are checked against the backends per the configured order, until an 'allow' or 'deny' decision can be made.
-
-If there is no decision after a full chain exhaustion, the login is rejected."""
-
 fields_mqtt_quic_listener_load_balancing_mode.desc:
 """0: Disabled, 1: SERVER_ID_IP, 2: SERVER_ID_FIXED. default: 0"""
 
@@ -1103,14 +1087,6 @@ See: https://erlang.org/doc/man/inet.html#setopts-2"""
 fields_tcp_opts_active_n.label:
 """active_n"""
 
-listener_authentication.desc:
-"""Per-listener authentication override.
-Authentication can be one single authenticator instance or a chain of authenticators as an array.
-When authenticating a login (username, client ID, etc.) the authenticators are checked in the configured order."""
-
-listener_authentication.label:
-"""Per-listener authentication override"""
-
 fields_trace_payload_encode.desc:
 """Determine the format of the payload format in the trace file.<br/>
 `text`: Text-based protocol or plain text protocol.