فهرست منبع

Merge pull request #5319 from tigercl/feat/authn-http-api

feat(authn): provide http api and improve update mechanism
tigercl 4 سال پیش
والد
کامیت
4dea41f8a2

+ 6 - 8
apps/emqx_authn/etc/emqx_authn.conf

@@ -1,13 +1,11 @@
 emqx_authn: {
     enable: false
     authenticators: [
-    #     {
-    #         name: "authenticator1"
-    #         mechanism: password-based
-    #         config: {
-    #             server_type: built-in-database
-    #             user_id_type: clientid
-    #         }
-    #     }
+        # {
+        #     name: "authenticator1"
+        #     mechanism: password-based
+        #     server_type: built-in-database
+        #     user_id_type: clientid
+        # }
     ]
 }

+ 6 - 7
apps/emqx_authn/include/emqx_authn.hrl

@@ -17,21 +17,20 @@
 -define(APP, emqx_authn).
 -define(CHAIN, <<"mqtt">>).
 
--type chain_id() :: binary().
--type authenticator_name() :: binary().
--type mechanism() :: 'password-based' | jwt | scram.
+-define(VER_1, <<"1">>).
+-define(VER_2, <<"2">>).
 
 -record(authenticator,
-        { name :: authenticator_name()
-        , mechanism :: mechanism()
+        { id :: binary()
+        , name :: binary()
         , provider :: module()
         , config :: map()
         , state :: map()
         }).
 
 -record(chain,
-        { id :: chain_id()
-        , authenticators :: [{authenticator_name(), #authenticator{}}]
+        { id :: binary()
+        , authenticators :: [{binary(), binary(), #authenticator{}}]
         , created_at :: integer()
         }).
 

+ 159 - 147
apps/emqx_authn/src/emqx_authn.erl

@@ -31,10 +31,9 @@
         , create_authenticator/2
         , delete_authenticator/2
         , update_authenticator/3
+        , update_or_create_authenticator/3
         , lookup_authenticator/2
         , list_authenticators/1
-        , move_authenticator_to_the_front/2
-        , move_authenticator_to_the_end/2
         , move_authenticator_to_the_nth/3
         ]).
 
@@ -95,7 +94,7 @@ authenticate(Credential, _AuthResult) ->
 
 do_authenticate([], _) ->
     {stop, {error, not_authorized}};
-do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], Credential) ->
+do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) ->
     case Provider:authenticate(Credential, State) of
         ignore ->
             do_authenticate(More, Credential);
@@ -130,7 +129,7 @@ delete_chain(ID) ->
                 [] ->
                     {error, {not_found, {chain, ID}}};
                 [#chain{authenticators = Authenticators}] ->
-                    _ = [do_delete_authenticator(Authenticator) || {_, Authenticator} <- Authenticators],
+                    _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
                     mnesia:delete(?CHAIN_TAB, ID, write)
             end
         end).
@@ -147,25 +146,21 @@ list_chains() ->
     Chains = ets:tab2list(?CHAIN_TAB),
     {ok, [serialize_chain(Chain) || Chain <- Chains]}.
 
-create_authenticator(ChainID, #{name := Name,
-                                mechanism := Mechanism,
-                                config := Config}) ->
+create_authenticator(ChainID, #{name := Name} = Config) ->
     UpdateFun =
         fun(Chain = #chain{authenticators = Authenticators}) ->
-            case lists:keymember(Name, 1, Authenticators) of
+            case lists:keymember(Name, 2, Authenticators) of
                 true ->
-                    {error, {already_exists, {authenticator, Name}}};
+                    {error, name_has_be_used};
                 false ->
-                    Provider = authenticator_provider(Mechanism, Config),
-                    case Provider:create(ChainID, Name, Config) of
-                        {ok, State} ->
-                            Authenticator = #authenticator{name = Name,
-                                                           mechanism = Mechanism,
-                                                           provider = Provider,
-                                                           config = Config,
-                                                           state = State},
-                            NChain = Chain#chain{authenticators = Authenticators ++ [{Name, Authenticator}]},
-                            ok = mnesia:write(?CHAIN_TAB, NChain, write),
+                    AlreadyExist = fun(ID) ->
+                                       lists:keymember(ID, 1, Authenticators)
+                                   end,
+                    AuthenticatorID = gen_id(AlreadyExist),
+                    case do_create_authenticator(ChainID, AuthenticatorID, Config) of
+                        {ok, Authenticator} ->
+                            NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}],
+                            ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
                             {ok, serialize_authenticator(Authenticator)};
                         {error, Reason} ->
                             {error, Reason}
@@ -174,12 +169,12 @@ create_authenticator(ChainID, #{name := Name,
         end,
     update_chain(ChainID, UpdateFun).
 
-delete_authenticator(ChainID, AuthenticatorName) ->
+delete_authenticator(ChainID, AuthenticatorID) ->
     UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case lists:keytake(AuthenticatorName, 1, Authenticators) of
+                    case lists:keytake(AuthenticatorID, 1, Authenticators) of
                         false ->
-                            {error, {not_found, {authenticator, AuthenticatorName}}};
-                        {value, {_, Authenticator}, NAuthenticators} ->
+                            {error, {not_found, {authenticator, AuthenticatorID}}};
+                        {value, {_, _, Authenticator}, NAuthenticators} ->
                             _ = do_delete_authenticator(Authenticator),
                             NChain = Chain#chain{authenticators = NAuthenticators},
                             mnesia:write(?CHAIN_TAB, NChain, write)
@@ -187,38 +182,86 @@ delete_authenticator(ChainID, AuthenticatorName) ->
                 end,
     update_chain(ChainID, UpdateFun).
 
-update_authenticator(ChainID, AuthenticatorName, Config) ->
+update_authenticator(ChainID, AuthenticatorID, Config) ->
+    do_update_authenticator(ChainID, AuthenticatorID, Config, false).
+
+update_or_create_authenticator(ChainID, AuthenticatorID, Config) ->
+    do_update_authenticator(ChainID, AuthenticatorID, Config, true).
+
+do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
     UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
-                        undefined ->
-                            {error, {not_found, {authenticator, AuthenticatorName}}};
-                        #authenticator{provider = Provider,
-                                       config   = OriginalConfig,
-                                       state    = State} = Authenticator ->
-                            NewConfig = maps:merge(OriginalConfig, Config),
-                            case Provider:update(ChainID, AuthenticatorName, NewConfig, State) of
-                                {ok, NState} ->
-                                    NAuthenticator = Authenticator#authenticator{config = NewConfig,
-                                                                                 state = NState},
-                                    NAuthenticators = update_value(AuthenticatorName, NAuthenticator, Authenticators),
-                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
-                                    {ok, serialize_authenticator(NAuthenticator)};
-                                {error, Reason} ->
-                                    {error, Reason}
+                    case lists:keytake(AuthenticatorID, 1, Authenticators) of
+                        false ->
+                            case CreateWhenNotFound of
+                                true ->
+                                    case lists:keymember(NewName, 2, Authenticators) of
+                                        true ->
+                                            {error, name_has_be_used};
+                                        false ->
+                                            case do_create_authenticator(ChainID, AuthenticatorID, Config) of
+                                                {ok, Authenticator} ->
+                                                    NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}],
+                                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write),
+                                                    {ok, serialize_authenticator(Authenticator)};
+                                                {error, Reason} ->
+                                                    {error, Reason}
+                                            end
+                                        end;
+                                false ->
+                                    {error, {not_found, {authenticator, AuthenticatorID}}}
+                            end;
+                        {value,
+                         {_, _, #authenticator{provider = Provider,
+                                               state    = #{version := Version} = State} = Authenticator},
+                         Others} ->
+                            case lists:keymember(NewName, 2, Others) of
+                                true ->
+                                    {error, name_has_be_used};
+                                false ->
+                                    case (NewProvider = authenticator_provider(Config)) =:= Provider of
+                                        true ->
+                                            Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
+                                            case Provider:update(Config#{'_unique' => Unique}, State) of
+                                                {ok, NewState} ->
+                                                    NewAuthenticator = Authenticator#authenticator{name = NewName,
+                                                                                                   config = Config,
+                                                                                                   state = switch_version(NewState)},
+                                                    NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
+                                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write),
+                                                    {ok, serialize_authenticator(NewAuthenticator)};
+                                                {error, Reason} ->
+                                                    {error, Reason}
+                                            end;
+                                        false ->
+                                            Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
+                                            case NewProvider:create(Config#{'_unique' => Unique}) of
+                                                {ok, NewState} ->
+                                                    NewAuthenticator = Authenticator#authenticator{name = NewName,
+                                                                                                   provider = NewProvider,
+                                                                                                   config = Config,
+                                                                                                   state = switch_version(NewState)},
+                                                    NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
+                                                    ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write),
+                                                    _ = Provider:destroy(State),
+                                                    {ok, serialize_authenticator(NewAuthenticator)};
+                                                {error, Reason} ->
+                                                    {error, Reason}
+                                            end
+                                    end
                             end
                     end
-                 end,
+                end,
     update_chain(ChainID, UpdateFun).
 
-lookup_authenticator(ChainID, AuthenticatorName) ->
+lookup_authenticator(ChainID, AuthenticatorID) ->
     case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
         [] ->
             {error, {not_found, {chain, ChainID}}};
         [#chain{authenticators = Authenticators}] ->
-            case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
-                undefined ->
-                    {error, {not_found, {authenticator, AuthenticatorName}}};
-                Authenticator ->
+            case lists:keyfind(AuthenticatorID, 1, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                {_, _, Authenticator} ->
                     {ok, serialize_authenticator(Authenticator)}
             end
     end.
@@ -231,33 +274,9 @@ list_authenticators(ChainID) ->
             {ok, serialize_authenticators(Authenticators)}
     end.
 
-move_authenticator_to_the_front(ChainID, AuthenticatorName) ->
-    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case move_authenticator_to_the_front_(AuthenticatorName, Authenticators) of
-                        {ok, NAuthenticators} ->
-                            NChain = Chain#chain{authenticators = NAuthenticators},
-                            mnesia:write(?CHAIN_TAB, NChain, write);
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-move_authenticator_to_the_end(ChainID, AuthenticatorName) ->
-    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case move_authenticator_to_the_end_(AuthenticatorName, Authenticators) of
-                        {ok, NAuthenticators} ->
-                            NChain = Chain#chain{authenticators = NAuthenticators},
-                            mnesia:write(?CHAIN_TAB, NChain, write);
-                        {error, Reason} ->
-                            {error, Reason}
-                    end
-                 end,
-    update_chain(ChainID, UpdateFun).
-
-move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) ->
+move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) ->
     UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N) of
+                    case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) of
                         {ok, NAuthenticators} ->
                             NChain = Chain#chain{authenticators = NAuthenticators},
                             mnesia:write(?CHAIN_TAB, NChain, write);
@@ -267,84 +286,94 @@ move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) ->
                  end,
     update_chain(ChainID, UpdateFun).
 
-import_users(ChainID, AuthenticatorName, Filename) ->
-    call_authenticator(ChainID, AuthenticatorName, import_users, [Filename]).
+import_users(ChainID, AuthenticatorID, Filename) ->
+    call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]).
 
-add_user(ChainID, AuthenticatorName, UserInfo) ->
-    call_authenticator(ChainID, AuthenticatorName, add_user, [UserInfo]).
+add_user(ChainID, AuthenticatorID, UserInfo) ->
+    call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]).
 
-delete_user(ChainID, AuthenticatorName, UserID) ->
-    call_authenticator(ChainID, AuthenticatorName, delete_user, [UserID]).
+delete_user(ChainID, AuthenticatorID, UserID) ->
+    call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]).
 
-update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) ->
-    call_authenticator(ChainID, AuthenticatorName, update_user, [UserID, NewUserInfo]).
+update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
+    call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]).
 
-lookup_user(ChainID, AuthenticatorName, UserID) ->
-    call_authenticator(ChainID, AuthenticatorName, lookup_user, [UserID]).
+lookup_user(ChainID, AuthenticatorID, UserID) ->
+    call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]).
 
-list_users(ChainID, AuthenticatorName) ->
-    call_authenticator(ChainID, AuthenticatorName, list_users, []).
+list_users(ChainID, AuthenticatorID) ->
+    call_authenticator(ChainID, AuthenticatorID, list_users, []).
 
 %%------------------------------------------------------------------------------
 %% Internal functions
 %%------------------------------------------------------------------------------
 
-authenticator_provider('password-based', #{server_type := 'built-in-database'}) ->
+authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) ->
     emqx_authn_mnesia;
-authenticator_provider('password-based', #{server_type := 'mysql'}) ->
+authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) ->
     emqx_authn_mysql;
-authenticator_provider('password-based', #{server_type := 'pgsql'}) ->
+authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
     emqx_authn_pgsql;
-authenticator_provider('password-based', #{server_type := 'http-server'}) ->
+authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
     emqx_authn_http;
-authenticator_provider(jwt, _) ->
+authenticator_provider(#{mechanism := jwt}) ->
     emqx_authn_jwt;
-authenticator_provider(scram, #{server_type := 'built-in-database'}) ->
+authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) ->
     emqx_enhanced_authn_scram_mnesia.
 
+gen_id(AlreadyExist) ->
+    ID = list_to_binary(emqx_rule_id:gen()),
+    case AlreadyExist(ID) of
+        true -> gen_id(AlreadyExist);
+        false -> ID
+    end.
+
+switch_version(State = #{version := ?VER_1}) ->
+    State#{version := ?VER_2};
+switch_version(State = #{version := ?VER_2}) ->
+    State#{version := ?VER_1};
+switch_version(State) ->
+    State#{version => ?VER_1}.
+
+do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
+    Provider = authenticator_provider(Config),
+    Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
+    case Provider:create(Config#{'_unique' => Unique}) of
+        {ok, State} ->
+            Authenticator = #authenticator{id = AuthenticatorID,
+                                           name = Name,
+                                           provider = Provider,
+                                           config = Config,
+                                           state = switch_version(State)},
+            {ok, Authenticator};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
 do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
-    Provider:destroy(State).
+    _ = Provider:destroy(State),
+    ok.
     
-update_value(Key, Value, List) ->
-    lists:keyreplace(Key, 1, List, {Key, Value}).
-
-move_authenticator_to_the_front_(AuthenticatorName, Authenticators) ->
-    move_authenticator_to_the_front_(AuthenticatorName, Authenticators, []).
-
-move_authenticator_to_the_front_(AuthenticatorName, [], _) ->
-    {error, {not_found, {authenticator, AuthenticatorName}}};
-move_authenticator_to_the_front_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) ->
-    {ok, [Authenticator | (lists:reverse(Passed) ++ More)]};
-move_authenticator_to_the_front_(AuthenticatorName, [Authenticator | More], Passed) ->
-    move_authenticator_to_the_front_(AuthenticatorName, More, [Authenticator | Passed]).
-
-move_authenticator_to_the_end_(AuthenticatorName, Authenticators) ->
-    move_authenticator_to_the_end_(AuthenticatorName, Authenticators, []).
-
-move_authenticator_to_the_end_(AuthenticatorName, [], _) ->
-    {error, {not_found, {authenticator, AuthenticatorName}}};
-move_authenticator_to_the_end_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], Passed) ->
-    {ok, lists:reverse(Passed) ++ More ++ [Authenticator]};
-move_authenticator_to_the_end_(AuthenticatorName, [Authenticator | More], Passed) ->
-    move_authenticator_to_the_end_(AuthenticatorName, More, [Authenticator | Passed]).
-
-move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N)
+replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
+    lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
+
+move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N)
   when N =< length(Authenticators) andalso N > 0 ->
-    move_authenticator_to_the_nth_(AuthenticatorName, Authenticators, N, []);
+    move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N, []);
 move_authenticator_to_the_nth_(_, _, _) ->
     {error, out_of_range}.
 
-move_authenticator_to_the_nth_(AuthenticatorName, [], _, _) ->
-    {error, {not_found, {authenticator, AuthenticatorName}}};
-move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed)
+move_authenticator_to_the_nth_(AuthenticatorID, [], _, _) ->
+    {error, {not_found, {authenticator, AuthenticatorID}}};
+move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed)
   when N =< length(Passed) ->
     {L1, L2} = lists:split(N - 1, lists:reverse(Passed)),
     {ok, L1 ++ [Authenticator] ++ L2 ++ More};
-move_authenticator_to_the_nth_(AuthenticatorName, [{AuthenticatorName, _} = Authenticator | More], N, Passed) ->
+move_authenticator_to_the_nth_(AuthenticatorID, [{AuthenticatorID, _, _} = Authenticator | More], N, Passed) ->
     {L1, L2} = lists:split(N - length(Passed) - 1, More),
     {ok, lists:reverse(Passed) ++ L1 ++ [Authenticator] ++ L2};
-move_authenticator_to_the_nth_(AuthenticatorName, [Authenticator | More], N, Passed) ->
-    move_authenticator_to_the_nth_(AuthenticatorName, More, N, [Authenticator | Passed]).
+move_authenticator_to_the_nth_(AuthenticatorID, [Authenticator | More], N, Passed) ->
+    move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]).
 
 update_chain(ChainID, UpdateFun) ->
     trans(
@@ -357,24 +386,15 @@ update_chain(ChainID, UpdateFun) ->
             end
         end).
 
-% lookup_chain_by_listener(ListenerID, AuthNType) ->
-%     case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of
-%         [] ->
-%             {error, not_found};
-%         [#binding{chain_id = ChainID}] ->
-%             {ok, ChainID}
-%     end.
-
-
-call_authenticator(ChainID, AuthenticatorName, Func, Args) ->
+call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
     case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
         [] ->
             {error, {not_found, {chain, ChainID}}};
         [#chain{authenticators = Authenticators}] ->
-            case proplists:get_value(AuthenticatorName, Authenticators, undefined) of
-                undefined ->
-                    {error, {not_found, {authenticator, AuthenticatorName}}};
-                #authenticator{provider = Provider, state = State} ->
+            case lists:keyfind(AuthenticatorID, 1, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                {_, _, #authenticator{provider = Provider, state = State}} ->
                     case erlang:function_exported(Provider, Func, length(Args) + 1) of
                         true ->
                             erlang:apply(Provider, Func, Args ++ [State]);
@@ -391,20 +411,12 @@ serialize_chain(#chain{id = ID,
       authenticators => serialize_authenticators(Authenticators),
       created_at => CreatedAt}.
 
-% serialize_binding(#binding{bound = {ListenerID, _},
-%                            chain_id = ChainID}) ->
-%     #{listener_id => ListenerID,
-%       chain_id => ChainID}.
-
 serialize_authenticators(Authenticators) ->
-    [serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators].
+    [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
 
-serialize_authenticator(#authenticator{name = Name,
-                                       mechanism = Mechanism,
+serialize_authenticator(#authenticator{id = ID,
                                        config = Config}) ->
-    #{name => Name,
-      mechanism => Mechanism,
-      config => Config}.
+    Config#{id => ID}.
 
 trans(Fun) ->
     trans(Fun, []).

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1061 - 293
apps/emqx_authn/src/emqx_authn_api.erl


+ 4 - 4
apps/emqx_authn/src/emqx_authn_app.erl

@@ -42,16 +42,16 @@ initialize() ->
                                                   authenticators => []}),
     initialize(AuthNConfig).
 
-initialize(#{enable := Enable, authenticators := Authenticators}) ->
+initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) ->
     {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}),
-    initialize_authenticators(Authenticators),
+    initialize_authenticators(AuthenticatorsConfig),
     Enable =:= true andalso emqx_authn:enable(),
     ok.
 
 initialize_authenticators([]) ->
     ok;
-initialize_authenticators([#{name := Name} = Authenticator | More]) ->
-    case emqx_authn:create_authenticator(?CHAIN, Authenticator) of
+initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) ->
+    case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of
         {ok, _} ->
             initialize_authenticators(More);
         {error, Reason} ->

+ 18 - 41
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -25,57 +25,34 @@
         , fields/1
         ]).
 
--reflect_type([ authenticator_name/0
-              ]).
+-export([ authenticator_name/1
+        ]).
 
-structs() -> ["emqx_authn"].
+structs() -> [ "emqx_authn" ].
 
 fields("emqx_authn") ->
     [ {enable, fun enable/1}
     , {authenticators, fun authenticators/1}
-    ];
-
-fields('password-based') ->
-    [ {name,      fun authenticator_name/1}
-    , {mechanism, {enum, ['password-based']}}
-    , {config,    hoconsc:t(hoconsc:union(
-                             [ hoconsc:ref(emqx_authn_mnesia, config)
-                             , hoconsc:ref(emqx_authn_mysql, config)
-                             , hoconsc:ref(emqx_authn_pgsql, config)
-                             , hoconsc:ref(emqx_authn_http, get)
-                             , hoconsc:ref(emqx_authn_http, post)
-                             ]))}
-    ];
-
-fields(jwt) ->
-    [ {name,      fun authenticator_name/1}
-    , {mechanism, {enum, [jwt]}}
-    , {config,    hoconsc:t(hoconsc:union(
-                             [ hoconsc:ref(emqx_authn_jwt, 'hmac-based')
-                             , hoconsc:ref(emqx_authn_jwt, 'public-key')
-                             , hoconsc:ref(emqx_authn_jwt, 'jwks')
-                             ]))}
-    ];
-
-fields(scram) ->
-    [ {name,      fun authenticator_name/1}
-    , {mechanism, {enum, [scram]}}
-    , {config,    hoconsc:t(hoconsc:union(
-                             [ hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
-                             ]))}
     ].
 
+authenticator_name(type) -> binary();
+authenticator_name(nullable) -> false;
+authenticator_name(_) -> undefined.
+
 enable(type) -> boolean();
-enable(defualt) -> false;
+enable(default) -> false;
 enable(_) -> undefined.
 
 authenticators(type) ->
-    hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based')
-                          , hoconsc:ref(?MODULE, jwt)
-                          , hoconsc:ref(?MODULE, scram)]});
+    hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
+                          , hoconsc:ref(emqx_authn_mysql, config)
+                          , hoconsc:ref(emqx_authn_pgsql, config)
+                          , hoconsc:ref(emqx_authn_http, get)
+                          , hoconsc:ref(emqx_authn_http, post)
+                          , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
+                          , hoconsc:ref(emqx_authn_jwt, 'public-key')
+                          , hoconsc:ref(emqx_authn_jwt, 'jwks')
+                          , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
+                          ]});
 authenticators(default) -> [];
 authenticators(_) -> undefined.
-
-authenticator_name(type) -> authenticator_name();
-authenticator_name(nullable) -> false;
-authenticator_name(_) -> undefined.

+ 17 - 13
apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl

@@ -26,8 +26,8 @@
         , fields/1
         ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -71,7 +71,9 @@ mnesia(copy) ->
 structs() -> [config].
 
 fields(config) ->
-    [ {server_type,     fun server_type/1}
+    [ {name,            fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,       {enum, [scram]}}
+    , {server_type,     fun server_type/1}
     , {algorithm,       fun algorithm/1}
     , {iteration_count, fun iteration_count/1}
     ].
@@ -80,7 +82,7 @@ server_type(type) -> hoconsc:enum(['built-in-database']);
 server_type(default) -> 'built-in-database';
 server_type(_) -> undefined.
 
-algorithm(type) -> hoconsc:enum([sha256, sha256]);
+algorithm(type) -> hoconsc:enum([sha256, sha512]);
 algorithm(default) -> sha256;
 algorithm(_) -> undefined.
 
@@ -92,16 +94,18 @@ iteration_count(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, Authenticator, #{algorithm := Algorithm,
-                                 iteration_count := IterationCount}) ->
-    State = #{user_group => {ChainID, Authenticator},
+create(#{ algorithm := Algorithm
+        , iteration_count := IterationCount
+        , '_unique' := Unique
+        }) ->
+    State = #{user_group => Unique,
               algorithm => Algorithm,
               iteration_count => IterationCount},
     {ok, State}.
 
-update(_ChainID, _Authenticator, _Config, _State) ->
-    {error, update_not_suppored}.
-
+update(Config, #{user_group := Unique}) ->
+    create(Config#{'_unique' => Unique}).
+    
 authenticate(#{auth_method := AuthMethod,
                auth_data := AuthData,
                auth_cache := AuthCache}, State) ->
@@ -129,8 +133,8 @@ destroy(#{user_group := UserGroup}) ->
         end).
 
 %% TODO: binary to atom
-add_user(#{<<"user_id">> := UserID,
-           <<"password">> := Password}, #{user_group := UserGroup} = State) ->
+add_user(#{user_id := UserID,
+           password := Password}, #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
             case mnesia:read(?TAB, {UserGroup, UserID}, write) of
@@ -153,7 +157,7 @@ delete_user(UserID, #{user_group := UserGroup}) ->
             end
         end).
 
-update_user(UserID, #{<<"password">> := Password},
+update_user(UserID, #{password := Password},
             #{user_group := UserGroup} = State) ->
     trans(
         fun() ->

+ 93 - 83
apps/emqx_authn/src/simple_authn/emqx_authn_http.erl

@@ -26,15 +26,8 @@
         , validations/0
         ]).
 
--type accept() :: 'application/json' | 'application/x-www-form-urlencoded'.
--type content_type() :: accept().
-
--reflect_type([ accept/0
-              , content_type/0
-              ]).
-
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -53,45 +46,55 @@ fields("") ->
 
 fields(get) ->
     [ {method,          #{type => get,
-                          default => get}}
+                          default => post}}
+    , {headers,         fun headers_no_content_type/1}
     ] ++ common_fields();
 
 fields(post) ->
     [ {method,          #{type => post,
-                          default => get}}
-    , {content_type,    fun content_type/1}
+                          default => post}}
+    , {headers,         fun headers/1}
     ] ++ common_fields().
 
 common_fields() ->
-    [ {server_type,     {enum, ['http-server']}}
+    [ {name,            fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,       {enum, ['password-based']}}
+    , {server_type,     {enum, ['http-server']}}
     , {url,             fun url/1}
-    , {accept,          fun accept/1}
-    , {headers,         fun headers/1}
     , {form_data,       fun form_data/1}
     , {request_timeout, fun request_timeout/1}
-    ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)).
+    ] ++ maps:to_list(maps:without([ base_url
+                                   , pool_type],
+                      maps:from_list(emqx_connector_http:fields(config)))).
 
 validations() ->
-    [ {check_ssl_opts, fun check_ssl_opts/1} ].
+    [ {check_ssl_opts, fun check_ssl_opts/1}
+    , {check_headers, fun check_headers/1}
+    ].
 
 url(type) -> binary();
 url(nullable) -> false;
 url(validate) -> [fun check_url/1];
 url(_) -> undefined.
 
-accept(type) -> accept();
-accept(default) -> 'application/json';
-accept(_) -> undefined.
-
-content_type(type) -> content_type();
-content_type(default) -> 'application/json';
-content_type(_) -> undefined.
-
-headers(type) -> list();
-headers(default) -> [];
+headers(type) -> map();
+headers(converter) ->
+    fun(Headers) ->
+       maps:merge(default_headers(), transform_header_name(Headers))
+    end;
+headers(default) -> default_headers();
 headers(_) -> undefined.
 
-form_data(type) -> binary();
+headers_no_content_type(type) -> map();
+headers_no_content_type(converter) ->
+    fun(Headers) ->
+       maps:merge(default_headers_no_content_type(), transform_header_name(Headers)) 
+    end;
+headers_no_content_type(default) -> default_headers_no_content_type();
+headers_no_content_type(_) -> undefined.
+
+%% TODO: Using map()
+form_data(type) -> map();
 form_data(nullable) -> false;
 form_data(validate) -> [fun check_form_data/1];
 form_data(_) -> undefined.
@@ -104,46 +107,41 @@ request_timeout(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, AuthenticatorName,
-        #{method := Method,
-          url := URL,
-          accept := Accept,
-          headers := Headers,
-          form_data := FormData,
-          request_timeout := RequestTimeout} = Config) ->
-    ContentType = maps:get(content_type, Config, undefined),
-    DefaultHeader0 = case ContentType of
-                         undefined -> #{};
-                         _ -> #{<<"content-type">> => atom_to_binary(ContentType, utf8)}
-                     end,
-    DefaultHeader = DefaultHeader0#{<<"accept">> => atom_to_binary(Accept, utf8)},
-    NHeaders = maps:to_list(maps:merge(DefaultHeader, maps:from_list(Headers))),
-    NFormData = preprocess_form_data(FormData),
+create(#{ method := Method
+        , url := URL
+        , headers := Headers
+        , form_data := FormData
+        , request_timeout := RequestTimeout
+        , '_unique' := Unique
+        } = Config) ->
     #{path := Path,
       query := Query} = URIMap = parse_url(URL),
-    BaseURL = generate_base_url(URIMap),
-    State = #{method          => Method,
-              path            => Path,
-              base_query      => cow_qs:parse_qs(list_to_binary(Query)),
-              accept          => Accept,
-              content_type    => ContentType,
-              headers         => NHeaders,
-              form_data       => NFormData,
-              request_timeout => RequestTimeout},
-    ResourceID = <<ChainID/binary, "/", AuthenticatorName/binary>>,
-    case emqx_resource:create_local(ResourceID, emqx_connector_http, Config#{base_url => BaseURL}) of
+    State = #{ method          => Method
+             , path            => Path
+             , base_query      => cow_qs:parse_qs(list_to_binary(Query))
+             , headers         => normalize_headers(Headers)
+             , form_data       => maps:to_list(FormData)
+             , request_timeout => RequestTimeout
+             },
+    case emqx_resource:create_local(Unique,
+                                    emqx_connector_http,
+                                    Config#{base_url => maps:remove(query, URIMap),
+                                            pool_type => random}) of
         {ok, _} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, already_created} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, Reason} ->
             {error, Reason}
     end.
 
-update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) ->
-    case emqx_resource:update_local(ResourceID, emqx_connector_http, Config, []) of
-        {ok, _} -> {ok, State};
-        {error, Reason} -> {error, Reason}
+update(Config, State) ->
+    case create(Config) of
+        {ok, NewState} ->
+            ok = destroy(State),
+            {ok, NewState};
+        {error, Reason} ->
+            {error, Reason}
     end.
 
 authenticate(#{auth_method := _}, _) ->
@@ -182,26 +180,38 @@ check_url(URL) ->
     end.
 
 check_form_data(FormData) ->
-    KVs = binary:split(FormData, [<<"&">>], [global]),
-    case false =:= lists:any(fun(T) -> T =:= <<>> end, KVs) of
-        true ->
-            NKVs = [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs],
-            false =:= 
-                lists:any(fun({K, V}) ->
-                              K =:= <<>> orelse V =:= <<>>;
-                             (_) ->
-                              true
-                          end, NKVs);
-        false ->
-            false
-    end.
+    lists:any(fun({_, V}) ->
+                  not is_binary(V)
+              end, maps:to_list(FormData)).
+
+default_headers() ->
+    maps:put(<<"content-type">>,
+             <<"application/json">>,
+             default_headers_no_content_type()).
+
+default_headers_no_content_type() ->
+    #{ <<"accept">> => <<"application/json">>
+     , <<"cache-control">> => <<"no-cache">>
+     , <<"connection">> => <<"keep-alive">>
+     , <<"keep-alive">> => <<"timeout=5">>
+     }.
+
+transform_header_name(Headers) ->
+    maps:fold(fun(K0, V, Acc) ->
+                  K = list_to_binary(string:to_lower(binary_to_list(K0))),
+                  maps:put(K, V, Acc)
+              end, #{}, Headers).
 
 check_ssl_opts(Conf) ->
     emqx_connector_http:check_ssl_opts("url", Conf).
 
-preprocess_form_data(FormData) ->
-    KVs = binary:split(FormData, [<<"&">>], [global]),
-    [list_to_tuple(binary:split(KV, [<<"=">>], [global])) || KV <- KVs].
+check_headers(Conf) ->
+    Method = hocon_schema:get_value("method", Conf),
+    Headers = hocon_schema:get_value("headers", Conf),
+    case Method =:= get andalso maps:get(<<"content-type">>, Headers, undefined) =/= undefined of
+        true -> false;
+        false -> true
+    end.
 
 parse_url(URL) ->
     {ok, URIMap} = emqx_http_lib:uri_parse(URL),
@@ -212,15 +222,12 @@ parse_url(URL) ->
             URIMap
     end.
 
-generate_base_url(#{scheme := Scheme,
-                    host := Host,
-                    port := Port}) ->
-    iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])).
+normalize_headers(Headers) ->
+    [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)].
 
 generate_request(Credential, #{method := Method,
                                path := Path,
                                base_query := BaseQuery,
-                               content_type := ContentType,
                                headers := Headers,
                                form_data := FormData0}) ->
     FormData = replace_placeholders(FormData0, Credential),
@@ -230,6 +237,7 @@ generate_request(Credential, #{method := Method,
             {NPath, Headers};
         post ->
             NPath = append_query(Path, BaseQuery),
+            ContentType = proplists:get_value(<<"content-type">>, Headers),
             Body = serialize_body(ContentType, FormData),
             {NPath, Headers, Body}
     end.
@@ -249,6 +257,8 @@ replace_placeholder(<<"${mqtt-username}">>, Credential) ->
     maps:get(username, Credential, undefined);
 replace_placeholder(<<"${mqtt-clientid}">>, Credential) ->
     maps:get(clientid, Credential, undefined);
+replace_placeholder(<<"${mqtt-password}">>, Credential) ->
+    maps:get(password, Credential, undefined);
 replace_placeholder(<<"${ip-address}">>, Credential) ->
     maps:get(peerhost, Credential, undefined);
 replace_placeholder(<<"${cert-subject}">>, Credential) ->
@@ -272,9 +282,9 @@ qs([], Acc) ->
 qs([{K, V} | More], Acc) ->
     qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]).
 
-serialize_body('application/json', FormData) ->
+serialize_body(<<"application/json">>, FormData) ->
     emqx_json:encode(FormData);
-serialize_body('application/x-www-form-urlencoded', FormData) ->
+serialize_body(<<"application/x-www-form-urlencoded">>, FormData) ->
     qs(FormData).
 
 safely_parse_body(ContentType, Body) ->

+ 25 - 30
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -22,11 +22,10 @@
 
 -export([ structs/0
         , fields/1
-        , validations/0
         ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -49,27 +48,24 @@ fields('hmac-based') ->
     , {algorithm,             {enum, ['hmac-based']}}
     , {secret,                fun secret/1}
     , {secret_base64_encoded, fun secret_base64_encoded/1}
-    , {verify_claims,         fun verify_claims/1}
-    ];
+    ] ++ common_fields();
 
 fields('public-key') ->
     [ {use_jwks,              {enum, [false]}}
     , {algorithm,             {enum, ['public-key']}}
     , {certificate,           fun certificate/1}
-    , {verify_claims,         fun verify_claims/1}
-    ];
+    ] ++ common_fields();
 
 fields('jwks') ->
     [ {use_jwks,              {enum, [true]}}
     , {endpoint,              fun endpoint/1}
     , {refresh_interval,      fun refresh_interval/1}
-    , {verify_claims,         fun verify_claims/1}
     , {ssl,                   #{type => hoconsc:union(
                                          [ hoconsc:ref(?MODULE, ssl_enable)
                                          , hoconsc:ref(?MODULE, ssl_disable)
                                          ]),
                                 default => #{<<"enable">> => false}}}
-    ];
+    ] ++ common_fields();
 
 fields(ssl_enable) ->
     [ {enable,                 #{type => true}}
@@ -81,19 +77,19 @@ fields(ssl_enable) ->
     ];
 
 fields(ssl_disable) ->
-    [ {enable, #{type => false}} ];
+    [ {enable, #{type => false}} ].
 
-fields(claim) ->
-    [ {"$name", fun expected_claim_value/1} ].
-
-validations() ->
-    [ {check_verify_claims, fun check_verify_claims/1} ].
+common_fields() ->
+    [ {name,            fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,       {enum, [jwt]}}
+    , {verify_claims,   fun verify_claims/1}
+    ].
 
 secret(type) -> string();
 secret(_) -> undefined.
 
 secret_base64_encoded(type) -> boolean();
-secret_base64_encoded(defualt) -> false;
+secret_base64_encoded(default) -> false;
 secret_base64_encoded(_) -> undefined.
 
 certificate(type) -> string();
@@ -123,29 +119,31 @@ verify(_) -> undefined.
 server_name_indication(type) -> string();
 server_name_indication(_) -> undefined.
 
-verify_claims(type) -> hoconsc:array(hoconsc:ref(claim));
-verify_claims(default) -> [];
+verify_claims(type) -> list();
+verify_claims(default) -> #{};
+verify_claims(validate) -> [fun check_verify_claims/1];
+verify_claims(converter) ->
+    fun(VerifyClaims) ->
+        maps:to_list(VerifyClaims)
+    end;
 verify_claims(_) -> undefined.
 
-expected_claim_value(type) -> string();
-expected_claim_value(_) -> undefined.
-
 %%------------------------------------------------------------------------------
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(_ChainID, _AuthenticatorName, Config) ->
-    create(Config).
+create(#{verify_claims := VerifyClaims} = Config) ->
+    create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
 
-update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, #{jwk := Connector})
+update(#{use_jwks := false} = Config, #{jwk := Connector})
   when is_pid(Connector) ->
     _ = emqx_authn_jwks_connector:stop(Connector),
     create(Config);
 
-update(_ChainID, _AuthenticatorName, #{use_jwks := false} = Config, _) ->
+update(#{use_jwks := false} = Config, _) ->
     create(Config);
 
-update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Connector} = State)
+update(#{use_jwks := true} = Config, #{jwk := Connector} = State)
   when is_pid(Connector) ->
     ok = emqx_authn_jwks_connector:update(Connector, Config),
     case maps:get(verify_cliams, Config, undefined) of
@@ -155,7 +153,7 @@ update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Conn
             {ok, State#{verify_claims => handle_verify_claims(VerifyClaims)}}
     end;
 
-update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) ->
+update(#{use_jwks := true} = Config, _) ->
     create(Config).
 
 authenticate(#{auth_method := _}, _) ->
@@ -186,9 +184,6 @@ destroy(_) ->
 %% Internal functions
 %%--------------------------------------------------------------------
 
-create(#{verify_claims := VerifyClaims} = Config) ->
-    create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
-
 create2(#{use_jwks := false,
           algorithm := 'hmac-based',
           secret := Secret0,

+ 23 - 17
apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl

@@ -23,8 +23,8 @@
 
 -export([ structs/0, fields/1 ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -39,7 +39,7 @@
 
 -type user_id_type() :: clientid | username.
 
--type user_group() :: {chain_id(), authenticator_name()}.
+-type user_group() :: {binary(), binary()}.
 -type user_id() :: binary().
 
 -record(user_info,
@@ -81,8 +81,10 @@ mnesia(copy) ->
 structs() -> [config].
 
 fields(config) ->
-    [ {server_type, {enum, ['built-in-database']}}
-    , {user_id_type, fun user_id_type/1}
+    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,               {enum, ['password-based']}}
+    , {server_type,             {enum, ['built-in-database']}}
+    , {user_id_type,            fun user_id_type/1}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     ];
 
@@ -111,25 +113,29 @@ salt_rounds(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, AuthenticatorName, #{user_id_type := Type,
-                                     password_hash_algorithm := #{name := bcrypt,
-                                                                  salt_rounds := SaltRounds}}) ->
+create(#{ user_id_type := Type
+        , password_hash_algorithm := #{name := bcrypt,
+                                       salt_rounds := SaltRounds}
+        , '_unique' := Unique
+        }) ->
     {ok, _} = application:ensure_all_started(bcrypt),
-    State = #{user_group => {ChainID, AuthenticatorName},
+    State = #{user_group => Unique,
               user_id_type => Type,
               password_hash_algorithm => bcrypt,
               salt_rounds => SaltRounds},
     {ok, State};
 
-create(ChainID, AuthenticatorName, #{user_id_type := Type,
-                                     password_hash_algorithm := #{name := Name}}) ->
-    State = #{user_group => {ChainID, AuthenticatorName},
+create(#{ user_id_type := Type
+        , password_hash_algorithm := #{name := Name}
+        , '_unique' := Unique
+        }) ->
+    State = #{user_group => Unique,
               user_id_type => Type,
               password_hash_algorithm => Name},
     {ok, State}.
 
-update(ChainID, AuthenticatorName, Config, _State) ->
-    create(ChainID, AuthenticatorName, Config).
+update(Config, #{user_group := Unique}) ->
+    create(Config#{'_unique' => Unique}).
 
 authenticate(#{auth_method := _}, _) ->
     ignore;
@@ -172,8 +178,8 @@ import_users(Filename0, State) ->
             {error, {unsupported_file_format, Extension}}
     end.
 
-add_user(#{<<"user_id">> := UserID,
-           <<"password">> := Password},
+add_user(#{user_id := UserID,
+           password := Password},
          #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
@@ -197,7 +203,7 @@ delete_user(UserID, #{user_group := UserGroup}) ->
             end
         end).
 
-update_user(UserID, #{<<"password">> := Password},
+update_user(UserID, #{password := Password},
             #{user_group := UserGroup} = State) ->
     trans(
         fun() ->

+ 50 - 21
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -21,10 +21,12 @@
 
 -behaviour(hocon_schema).
 
--export([ structs/0, fields/1 ]).
+-export([ structs/0
+        , fields/1
+        ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -36,50 +38,77 @@
 structs() -> [config].
 
 fields(config) ->
-    [ {server_type,             {enum, [mysql]}}
+    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,               {enum, ['password-based']}}
+    , {server_type,             {enum, [mysql]}}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
-    , {salt_position,           {enum, [prefix, suffix]}}
+    , {salt_position,           fun salt_position/1}
     , {query,                   fun query/1}
     , {query_timeout,           fun query_timeout/1}
     ] ++ emqx_connector_schema_lib:relational_db_fields()
-    ++ emqx_connector_schema_lib:ssl_fields().
+    ++ emqx_connector_schema_lib:ssl_fields();
+
+fields(bcrypt) ->
+    [ {name, {enum, [bcrypt]}}
+    , {salt_rounds, fun salt_rounds/1}
+    ];
 
-password_hash_algorithm(type) -> string();
+fields(other_algorithms) ->
+    [ {name, {enum, [plain, md5, sha, sha256, sha512]}}
+    ].
+
+password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]};
+password_hash_algorithm(default) -> #{<<"name">> => sha256};
 password_hash_algorithm(_) -> undefined.
 
+salt_rounds(type) -> integer();
+salt_rounds(default) -> 10;
+salt_rounds(_) -> undefined.
+
+salt_position(type) -> {enum, [prefix, suffix]};
+salt_position(default) -> prefix;
+salt_position(_) -> undefined.
+
 query(type) -> string();
 query(nullable) -> false;
 query(_) -> undefined.
 
 query_timeout(type) -> integer();
-query_timeout(defualt) -> 5000;
+query_timeout(default) -> 5000;
 query_timeout(_) -> undefined.
 
 %%------------------------------------------------------------------------------
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, AuthenticatorName,
-        #{query := Query0,
-          password_hash_algorithm := Algorithm} = Config) ->
+create(#{ password_hash_algorithm := Algorithm
+        , salt_position := SaltPosition
+        , query := Query0
+        , query_timeout := QueryTimeout
+        , '_unique' := Unique
+        } = Config) ->
     {Query, PlaceHolders} = parse_query(Query0),
-    ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, AuthenticatorName])),
-    State = #{query => Query,
+    State = #{password_hash_algorithm => Algorithm,
+              salt_position => SaltPosition,
+              query => Query,
               placeholders => PlaceHolders,
-              password_hash_algorithm => Algorithm},
-    case emqx_resource:create_local(ResourceID, emqx_connector_mysql, Config) of
+              query_timeout => QueryTimeout},
+    case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of
         {ok, _} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, already_created} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, Reason} ->
             {error, Reason}
     end.
 
-update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = State) ->
-    case emqx_resource:update_local(ResourceID, emqx_connector_mysql, Config, []) of
-        {ok, _} -> {ok, State};
-        {error, Reason} -> {error, Reason}
+update(Config, State) ->
+    case create(Config) of
+        {ok, NewState} ->
+            ok = destroy(State),
+            {ok, NewState};
+        {error, Reason} ->
+            {error, Reason}
     end.
 
 authenticate(#{auth_method := _}, _) ->

+ 58 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl

@@ -0,0 +1,58 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2021 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_other_schema).
+
+-include("emqx_authn.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0
+        , fields/1
+        ]).
+
+structs() -> [ "filename", "position", "user_info", "new_user_info"].
+
+fields("filename") ->
+    [ {filename, fun filename/1} ];
+fields("position") ->
+    [ {position, fun position/1} ];
+fields("user_info") ->
+    [ {user_id, fun user_id/1}
+    , {password, fun password/1}
+    ];
+fields("new_user_info") ->
+    [ {password, fun password/1}
+    ].
+
+filename(type) -> string();
+filename(nullable) -> false;
+filename(_) -> undefined.
+
+position(type) -> integer();
+position(validate) -> [fun (Position) -> Position > 0 end];
+position(nullable) -> false;
+position(_) -> undefined.
+
+user_id(type) -> binary();
+user_id(nullable) -> false;
+user_id(_) -> undefined.
+
+password(type) -> binary();
+password(nullable) -> false;
+password(_) -> undefined.
+

+ 22 - 14
apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl

@@ -23,8 +23,8 @@
 
 -export([ structs/0, fields/1 ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -36,7 +36,9 @@
 structs() -> [config].
 
 fields(config) ->
-    [ {server_type,             {enum, [pgsql]}}
+    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,               {enum, ['password-based']}}
+    , {server_type,             {enum, [pgsql]}}
     , {password_hash_algorithm, fun password_hash_algorithm/1}
     , {salt_position,           {enum, [prefix, suffix]}}
     , {query,                   fun query/1}
@@ -54,26 +56,32 @@ query(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, ServiceName, #{query := Query0,
-                               password_hash_algorithm := Algorithm} = Config) ->
+create(#{ query := Query0
+        , password_hash_algorithm := Algorithm
+        , salt_position := SaltPosition
+        , '_unique' := Unique
+        } = Config) ->
     {Query, PlaceHolders} = parse_query(Query0),
-    ResourceID = iolist_to_binary(io_lib:format("~s/~s",[ChainID, ServiceName])),
     State = #{query => Query,
               placeholders => PlaceHolders,
-              password_hash_algorithm => Algorithm},
-    case emqx_resource:create_local(ResourceID, emqx_connector_pgsql, Config) of
+              password_hash_algorithm => Algorithm,
+              salt_position => SaltPosition},
+    case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of
         {ok, _} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, already_created} ->
-            {ok, State#{resource_id => ResourceID}};
+            {ok, State#{resource_id => Unique}};
         {error, Reason} ->
             {error, Reason}
     end.
 
-update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) ->
-    case emqx_resource:update_local(ResourceID, emqx_connector_pgsql, Config, []) of
-        {ok, _} -> {ok, State};
-        {error, Reason} -> {error, Reason}
+update(Config, State) ->
+    case create(Config) of
+        {ok, NewState} ->
+            ok = destroy(State),
+            {ok, NewState};
+        {error, Reason} ->
+            {error, Reason}
     end.
 
 authenticate(#{auth_method := _}, _) ->

+ 36 - 26
apps/emqx_authn/test/emqx_authn_SUITE.erl

@@ -54,34 +54,44 @@ t_authenticator(_) ->
     AuthenticatorName1 = <<"myauthenticator1">>,
     AuthenticatorConfig1 = #{name => AuthenticatorName1,
                              mechanism => 'password-based',
-                             config => #{
-                                 server_type => 'built-in-database',
-                                 user_id_type => username,
-                                 password_hash_algorithm => #{
-                                     name => sha256
-                                 }}},
-    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
-    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)),
-    ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
-    ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
+                             server_type => 'built-in-database',
+                             user_id_type => username,
+                             password_hash_algorithm => #{
+                                 name => sha256
+                             }},
+    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
+    ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)),
+    ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
+    ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
+
+    AuthenticatorConfig2 = #{name => AuthenticatorName1,
+                             mechanism => jwt,
+                             use_jwks => false,
+                             algorithm => 'hmac-based',
+                             secret => <<"abcdef">>,
+                             secret_base64_encoded => false,
+                             verify_claims => []},
+    {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
+
+    ID2 = <<"random">>,
+    ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
+    ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
 
     AuthenticatorName2 = <<"myauthenticator2">>,
-    AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2},
-    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)),
-    ?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)),
-    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)),
-    ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)),
-
-    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)),
-    ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
-    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)),
-    ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)),
-    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)),
-    ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
-    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)),
-    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)),
+    AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
+    {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
+    ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)),
+    {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}),
+
+    ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
+    ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
+
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)),
+    ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
+    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 3)),
+    ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
     ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
     ok.
 

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

@@ -39,15 +39,14 @@ end_per_suite(_) ->
 
 t_jwt_authenticator(_) ->
     AuthenticatorName = <<"myauthenticator">>,
-    Config = #{use_jwks => false,
+    Config = #{name => AuthenticatorName,
+               mechanism => jwt,
+               use_jwks => false,
                algorithm => 'hmac-based',
                secret => <<"abcdef">>,
                secret_base64_encoded => false,
                verify_claims => []},
-    AuthenticatorConfig = #{name => AuthenticatorName,
-                            mechanism => jwt,
-                            config => Config},
-    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
+    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
 
     Payload = #{<<"username">> => <<"myuser">>},
     JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
@@ -62,11 +61,11 @@ t_jwt_authenticator(_) ->
     %% secret_base64_encoded
     Config2 = Config#{secret => base64:encode(<<"abcdef">>),
                       secret_base64_encoded => true},
-    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)),
+    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
 
     Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
-    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)),
+    ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
 
@@ -109,7 +108,7 @@ t_jwt_authenticator(_) ->
     ClientInfo8 = ClientInfo#{password => JWS8},
     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)),
 
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.
 
 t_jwt_authenticator2(_) ->
@@ -117,14 +116,13 @@ t_jwt_authenticator2(_) ->
     PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
     PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
     AuthenticatorName = <<"myauthenticator">>,
-    Config = #{use_jwks => false,
+    Config = #{name => AuthenticatorName,
+               mechanism => jwt,
+               use_jwks => false,
                algorithm => 'public-key',
                certificate => PublicKey,
                verify_claims => []},
-    AuthenticatorConfig = #{name => AuthenticatorName,
-                            mechanism => jwt,
-                            config => Config},
-    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
+    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
 
     Payload = #{<<"username">> => <<"myuser">>},
     JWS = generate_jws('public-key', Payload, PrivateKey),
@@ -133,7 +131,7 @@ t_jwt_authenticator2(_) ->
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
     ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)),
 
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.
 
 generate_jws('hmac-based', Payload, Secret) ->

+ 53 - 57
apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl

@@ -41,18 +41,17 @@ t_mnesia_authenticator(_) ->
     AuthenticatorName = <<"myauthenticator">>,
     AuthenticatorConfig = #{name => AuthenticatorName,
                             mechanism => 'password-based',
-                            config => #{
-                                server_type => 'built-in-database',
-                                user_id_type => username,
-                                password_hash_algorithm => #{
-                                    name => sha256
-                                }}},
-    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
-
-    UserInfo = #{<<"user_id">> => <<"myuser">>,
-                 <<"password">> => <<"mypass">>},
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)),
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
+                            server_type => 'built-in-database',
+                            user_id_type => username,
+                            password_hash_algorithm => #{
+                                name => sha256
+                            }},
+    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
+
+    UserInfo = #{user_id => <<"myuser">>,
+                 password => <<"mypass">>},
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
 
     ClientInfo = #{zone => external,
                    username => <<"myuser">>,
@@ -69,40 +68,39 @@ t_mnesia_authenticator(_) ->
     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
     ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
 
-    UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>},
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)),
+    UserInfo2 = UserInfo#{password => <<"mypass2">>},
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
     ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
 
-    ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
-    ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
+    ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
+    ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
 
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)),
-    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
+    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
 
-    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
-    ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
+    {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
+    ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
     ok.
 
 t_import(_) ->
     AuthenticatorName = <<"myauthenticator">>,
     AuthenticatorConfig = #{name => AuthenticatorName,
                             mechanism => 'password-based',
-                            config => #{
-                                server_type => 'built-in-database',
-                                user_id_type => username,
-                                password_hash_algorithm => #{
-                                    name => sha256
-                                }}},
-    ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
+                            server_type => 'built-in-database',
+                            user_id_type => username,
+                            password_hash_algorithm => #{
+                                name => sha256
+                            }},
+    {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
 
     Dir = code:lib_dir(emqx_authn, test),
-    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))),
-    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))),
-    ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)),
-    ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)),
+    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))),
+    ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
+    ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
+    ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
 
     ClientInfo1 = #{username => <<"myuser1">>,
 			        password => <<"mypassword1">>},
@@ -110,50 +108,48 @@ t_import(_) ->
     ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
                                password => <<"mypassword3">>},
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.
 
 t_multi_mnesia_authenticator(_) ->
     AuthenticatorName1 = <<"myauthenticator1">>,
     AuthenticatorConfig1 = #{name => AuthenticatorName1,
                              mechanism => 'password-based',
-                             config => #{
-                                 server_type => 'built-in-database',
-                                 user_id_type => username,
-                                 password_hash_algorithm => #{
-                                     name => sha256
-                                 }}},
+                             server_type => 'built-in-database',
+                             user_id_type => username,
+                             password_hash_algorithm => #{
+                                 name => sha256
+                             }},
     AuthenticatorName2 = <<"myauthenticator2">>,
     AuthenticatorConfig2 = #{name => AuthenticatorName2,
                              mechanism => 'password-based',
-                             config => #{
-                                 server_type => 'built-in-database',
-                                 user_id_type => clientid,
-                                 password_hash_algorithm => #{
-                                     name => sha256
-                                 }}},
-    ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
-    ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)),
+                             server_type => 'built-in-database',
+                             user_id_type => clientid,
+                             password_hash_algorithm => #{
+                                 name => sha256
+                             }},
+    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
+    {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
 
     ?assertEqual({ok, #{user_id => <<"myuser">>}},
-                 ?AUTH:add_user(?CHAIN, AuthenticatorName1,
-                                #{<<"user_id">> => <<"myuser">>,
-                                  <<"password">> => <<"mypass1">>})),
+                 ?AUTH:add_user(?CHAIN, ID1,
+                                #{user_id => <<"myuser">>,
+                                  password => <<"mypass1">>})),
     ?assertEqual({ok, #{user_id => <<"myclient">>}},
-                 ?AUTH:add_user(?CHAIN, AuthenticatorName2,
-                                #{<<"user_id">> => <<"myclient">>,
-                                  <<"password">> => <<"mypass2">>})),
+                 ?AUTH:add_user(?CHAIN, ID2,
+                                #{user_id => <<"myclient">>,
+                                  password => <<"mypass2">>})),
 
     ClientInfo1 = #{username => <<"myuser">>,
                     clientid => <<"myclient">>,
 			        password => <<"mypass1">>},
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
-    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)),
+    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)),
 
     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)),
     ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
     ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
 
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)),
-    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
+    ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
     ok.

+ 14 - 9
apps/emqx_connector/src/emqx_connector_http.erl

@@ -53,14 +53,15 @@ fields("") ->
     [{config, #{type => hoconsc:ref(?MODULE, config)}}];
 
 fields(config) ->
-    [ {base_url,        fun base_url/1}
-    , {connect_timeout, fun connect_timeout/1}
-    , {max_retries,     fun max_retries/1}
-    , {retry_interval,  fun retry_interval/1}
-    , {pool_type,       fun pool_type/1}
-    , {pool_size,       fun pool_size/1}
-    , {ssl_opts,        #{type => hoconsc:ref(?MODULE, ssl_opts),
-                          default => #{}}}
+    [ {base_url,          fun base_url/1}
+    , {connect_timeout,   fun connect_timeout/1}
+    , {max_retries,       fun max_retries/1}
+    , {retry_interval,    fun retry_interval/1}
+    , {pool_type,         fun pool_type/1}
+    , {pool_size,         fun pool_size/1}
+    , {enable_pipelining, fun enable_pipelining/1}
+    , {ssl_opts,          #{type => hoconsc:ref(?MODULE, ssl_opts),
+                            default => #{}}}
     ];
 
 fields(ssl_opts) ->
@@ -101,6 +102,10 @@ pool_size(type) -> non_neg_integer();
 pool_size(default) -> 8;
 pool_size(_) -> undefined.
 
+enable_pipelining(type) -> boolean();
+enable_pipelining(default) -> true;
+enable_pipelining(_) -> undefined.
+
 cacertfile(type) -> string();
 cacertfile(nullable) -> true;
 cacertfile(_) -> undefined.
@@ -147,7 +152,7 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
                , {pool_type, PoolType}
                , {pool_size, PoolSize}
                , {transport, Transport}
-               , {transport, NTransportOpts}],
+               , {transport_opts, NTransportOpts}],
     PoolName = emqx_plugin_libs_pool:pool_name(InstId),
     {ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts),
     {ok, #{pool_name => PoolName,

+ 1 - 1
rebar.config

@@ -43,7 +43,7 @@
 
 {deps,
     [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
-    , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.7"}}}
+    , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.8"}}}
     , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
     , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
     , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}