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

feat(authn http api): provide http api for authn and improve update mechanism

zhouzb 4 лет назад
Родитель
Сommit
327ff8636f

+ 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
+        }
     ]
 }

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

@@ -17,21 +17,21 @@
 -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()
+        , version :: binary()
         }).
 
 -record(chain,
-        { id :: chain_id()
-        , authenticators :: [{authenticator_name(), #authenticator{}}]
+        { id :: binary()
+        , authenticators :: [{binary(), binary(), #authenticator{}}]
         , created_at :: integer()
         }).
 

+ 153 - 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,80 @@ 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 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;
+                                false ->
+                                    {error, {not_found, {authenticator, AuthenticatorID}}}
+                            end;
+                        {value,
+                         {_, _, #authenticator{provider = Provider,
+                                               state    = #{version := Version} = State}},
+                         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, AuthenticatorID, Version},
+                                            case Provider:update(Config#{'_unique' => Unique}, State) of
+                                                {ok, NewState} ->
+                                                    NewAuthenticator = #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 ->
+                                            case NewProvider:create(Config#{'_unique' => {ChainID, AuthenticatorID, Version}}) of
+                                                {ok, NewState} ->
+                                                    NewAuthenticator = #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 +268,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 +280,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 +380,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 +405,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, []).

+ 748 - 70
apps/emqx_authn/src/emqx_authn_api.erl

@@ -20,12 +20,25 @@
 
 -include("emqx_authn.hrl").
 
--export([ api_spec/0 ]).
+-export([ api_spec/0
+        , authenticators/2
+        , authenticators2/2
+        , position/2
+        , import_users/2
+        , users/2
+        , users2/2
+        ]).
 
 api_spec() ->
-    {[authenticator_api()], definitions()}.
+    {[ authenticators_api()
+     , authenticators_api2()
+     , position_api()
+     , import_users_api()
+     , users_api()
+     , users2_api()
+     ], definitions()}.
 
-authenticator_api() ->
+authenticators_api() ->
     Example1 = #{name => <<"example">>,
                  mechanism => <<"password-based">>,
                  config => #{
@@ -86,24 +99,506 @@ authenticator_api() ->
             },
             responses => #{
                 <<"201">> => #{
-                    description => <<"Created successfully">>,
-                    content => #{}
+                    description => <<"Created">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"returned_authenticator">>)
+                        }
+                    }
+                }
+            }
+        },
+        get => #{
+            description => "List authenticators",
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => #{
+                                type => array,
+                                items => minirest:ref(<<"returned_authenticator">>)
+                            }
+                        }
+                    }
                 }
             }
         }
     },
     {"/authentication/authenticators", Metadata, authenticators}.
 
+authenticators_api2() ->
+    Metadata = #{
+        get => #{
+            description => "Get authenicator by id",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"returned_authenticator">>)
+                        }
+                    }
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        },
+        put => #{
+            description => "Update authenticator",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                       type => string
+                    },
+                    required => true
+                }
+            ],
+            requestBody => #{
+                content => #{
+                    'application/json' => #{
+                        schema => #{
+                            oneOf => [ minirest:ref(<<"password_based">>)
+                                     , minirest:ref(<<"jwt">>)
+                                     , minirest:ref(<<"scram">>)
+                                     ]   
+                        }
+                    }
+                }
+            },
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"returned_authenticator">>)
+                        }
+                    }
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        },
+        delete => #{
+            description => "Delete authenticator",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                       type => string
+                    },
+                    required => true
+                }
+            ],
+            responses => #{
+                <<"204">> => #{
+                    description => <<"No Content">>
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {"/authentication/authenticators/:id", Metadata, authenticators2}.
+
+position_api() ->
+    Metadata = #{
+        post => #{
+            description => "Change the order of authenticators",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            requestBody => #{
+                content => #{
+                    'application/json' => #{
+                        schema => #{
+                            type => object,
+                            required => [position],
+                            properties => #{
+                                position => #{
+                                    type => integer,
+                                    example => 1
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            responses => #{
+                <<"204">> => #{
+                    description => <<"No Content">>
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {"/authentication/authenticators/:id/position", Metadata, position}.
+
+import_users_api() ->
+    Metadata = #{
+        post => #{
+            description => "Import users from json/csv file",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            requestBody => #{
+                content => #{
+                    'application/json' => #{
+                        schema => #{
+                            type => object,
+                            required => [filename],
+                            properties => #{
+                                filename => #{
+                                    type => string
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            responses => #{
+                <<"204">> => #{
+                    description => <<"No Content">>
+                },
+                <<"400">> => #{
+                    description => <<"Bad Request">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {"/authentication/authenticators/:id/import-users", Metadata, import_users}.
+
+users_api() ->
+    Metadata = #{
+        post => #{
+            description => "Add user",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            requestBody => #{
+                content => #{
+                    'application/json' => #{
+                        schema => #{
+                            type => object,
+                            required => [user_id, password],
+                            properties => #{
+                                user_id => #{
+                                    type => string
+                                },
+                                password => #{
+                                    type => string
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            responses => #{
+                <<"201">> => #{
+                    description => <<"Created">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => #{
+                                type => object,
+                                required => [user_id],
+                                properties => #{
+                                    user_id => #{
+                                        type => string
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                <<"400">> => #{
+                    description => <<"Bad Request">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        },
+        get => #{
+            description => "List users",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => #{
+                                type => array,
+                                items => #{
+                                    type => object,
+                                    required => [user_id],
+                                    properties => #{
+                                        user_id => #{
+                                            type => string
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {"/authentication/authenticators/:id/users", Metadata, users}.
+
+users2_api() ->
+    Metadata = #{
+        patch => #{
+            description => "Update user",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                },
+                #{
+                    name => user_id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            requestBody => #{
+                content => #{
+                    'application/json' => #{
+                        schema => #{
+                            type => object,
+                            required => [password],
+                            properties => #{
+                                password => #{
+                                    type => string
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => #{
+                                type => array,
+                                items => #{
+                                    type => object,
+                                    required => [user_id],
+                                    properties => #{
+                                        user_id => #{
+                                            type => string
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        },
+        get => #{
+            description => "Get user info",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                },
+                #{
+                    name => user_id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            responses => #{
+                <<"200">> => #{
+                    description => <<"OK">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => #{
+                                type => array,
+                                items => #{
+                                    type => object,
+                                    required => [user_id],
+                                    properties => #{
+                                        user_id => #{
+                                            type => string
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        },
+        delete => #{
+            description => "Delete user",
+            parameters => [
+                #{
+                    name => id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                },
+                #{
+                    name => user_id,
+                    in => path,
+                    schema => #{
+                        type => string
+                    },
+                    required => true
+                }
+            ],
+            responses => #{
+                <<"204">> => #{
+                    description => <<"No Content">>
+                },
+                <<"404">> => #{
+                    description => <<"Not Found">>,
+                    content => #{
+                        'application/json' => #{
+                            schema => minirest:ref(<<"error">>)
+                        }
+                    }
+                }
+            }
+        }
+    },
+    {"/authentication/authenticators/:id/users/:user_id", Metadata, users2}.
+
+
 definitions() ->
     AuthenticatorDef = #{
+        oneOf => [ minirest:ref(<<"password_based">>)
+                 , minirest:ref(<<"jwt">>)
+                 , minirest:ref(<<"scram">>)
+                 ]   
+    },
+
+    ReturnedAuthenticatorDef = #{
         allOf => [
             #{
                 type => object,
-                required => [name],
                 properties => #{
-                    name => #{
-                        type => string,
-                        example => "exmaple"
+                    id => #{
+                        type => string
                     }
                 }
             },
@@ -111,99 +606,108 @@ definitions() ->
                 oneOf => [ minirest:ref(<<"password_based">>)
                          , minirest:ref(<<"jwt">>)
                          , minirest:ref(<<"scram">>)
-                ]    
+                         ]    
             }
         ]
     },
 
     PasswordBasedDef = #{
-        type => object,
-        properties => #{
-            mechanism => #{
-                type => string,
-                enum => [<<"password-based">>],
-                example => <<"password-based">>
+        allOf => [
+            #{
+                type => object,
+                required => [name, mechanism],
+                properties => #{
+                    name => #{
+                        type => string,
+                        example => "exmaple"
+                    },
+                    mechanism => #{
+                        type => string,
+                        enum => [<<"password-based">>],
+                        example => <<"password-based">>
+                    }
+                }
             },
-            config => #{
+            #{
                 oneOf => [ minirest:ref(<<"password_based_built_in_database">>)
                          , minirest:ref(<<"password_based_mysql">>)
                          , minirest:ref(<<"password_based_pgsql">>)
                          , minirest:ref(<<"password_based_http_server">>)
-                         ]
+                         ]    
             }
-        }
+        ]
     },
 
     JWTDef = #{
         type => object,
+        required => [name, mechanism],
         properties => #{
+            name => #{
+                type => string,
+                example => "exmaple"
+            },
             mechanism => #{
                 type => string,
                 enum => [<<"jwt">>],
                 example => <<"jwt">>
             },
-            config => #{
+            use_jwks => #{
+                type => boolean,
+                default => false,
+                example => false
+            },
+            algorithm => #{
+                type => string,
+                enum => [<<"hmac-based">>, <<"public-key">>],
+                default => <<"hmac-based">>,
+                example => <<"hmac-based">>
+            },
+            secret => #{
+                type => string
+            },
+            secret_base64_encoded => #{
+                type => boolean,
+                default => false
+            },
+            certificate => #{
+                type => string
+            },
+            verify_claims => #{
                 type => object,
-                properties => #{
-                    use_jwks => #{
-                        type => boolean,
-                        default => false,
-                        example => false
-                    },
-                    algorithm => #{
-                        type => string,
-                        enum => [<<"hmac-based">>, <<"public-key">>],
-                        default => <<"hmac-based">>,
-                        example => <<"hmac-based">>
-                    },
-                    secret => #{
-                        type => string
-                    },
-                    secret_base64_encoded => #{
-                        type => boolean,
-                        default => false
-                    },
-                    certificate => #{
-                        type => string
-                    },
-                    verify_claims => #{
-                        type => object,
-                        additionalProperties => #{
-                            type => string
-                        }
-                    },
-                    ssl => minirest:ref(<<"ssl">>)
-                } 
-            }
+                additionalProperties => #{
+                    type => string
+                }
+            },
+            ssl => minirest:ref(<<"ssl">>)
         }
     },
     
     SCRAMDef = #{
         type => object,
+        required => [name, mechanism],
         properties => #{
+            name => #{
+                type => string,
+                example => "exmaple"
+            },
             mechanism => #{
                 type => string,
                 enum => [<<"scram">>],
                 example => <<"scram">>
             },
-            config => #{
-                type => object,
-                properties => #{
-                    server_type => #{
-                        type => string,
-                        enum => [<<"built-in-database">>],
-                        default => <<"built-in-database">>
-                    },
-                    algorithm => #{
-                        type => string,
-                        enum => [<<"sha256">>, <<"sha512">>],
-                        default => <<"sha256">>
-                    },
-                    iteration_count => #{
-                        type => integer,
-                        default => 4096
-                    }
-                }
+            server_type => #{
+                type => string,
+                enum => [<<"built-in-database">>],
+                default => <<"built-in-database">>
+            },
+            algorithm => #{
+                type => string,
+                enum => [<<"sha256">>, <<"sha512">>],
+                default => <<"sha256">>
+            },
+            iteration_count => #{
+                type => integer,
+                default => 4096
             }
         }
     },
@@ -410,7 +914,7 @@ definitions() ->
                 properties => #{
                     enable => #{
                         type => boolean,
-                        default => false    
+                        default => false
                     },
                     hostname => #{
                         type => string
@@ -420,7 +924,22 @@ definitions() ->
         }
     },
 
+    ErrorDef = #{
+        type => object,
+        properties => #{
+            code => #{
+                type => string,
+                enum => [<<"NOT_FOUND">>],
+                example => <<"NOT_FOUND">>
+            },
+            message => #{
+                type => string
+            }
+        }
+    },
+
     [ #{<<"authenticator">> => AuthenticatorDef}
+    , #{<<"returned_authenticator">> => ReturnedAuthenticatorDef}
     , #{<<"password_based">> => PasswordBasedDef}
     , #{<<"jwt">> => JWTDef}
     , #{<<"scram">> => SCRAMDef}
@@ -430,4 +949,163 @@ definitions() ->
     , #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef}
     , #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef}
     , #{<<"ssl">> => SSLDef}
+    , #{<<"error">> => ErrorDef}
     ].
+
+authenticators(post, Request) ->
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    AuthenticatorConfig = emqx_json:decode(Body, [return_maps]),
+    Config = #{<<"emqx_authn">> => #{
+                   <<"authenticators">> => [AuthenticatorConfig]
+               }},
+    NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
+                                       #{nullable => true}),
+    #{emqx_authn := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig),
+    case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of
+        {ok, Authenticator2} ->
+            {201, Authenticator2};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end;
+authenticators(get, _Request) ->
+    {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN),
+    {200, Authenticators}.
+
+authenticators2(get, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of
+        {ok, Authenticator} ->
+           {200, Authenticator};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end;
+authenticators2(put, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    AuthenticatorConfig = emqx_json:decode(Body, [return_maps]),
+    Config = #{<<"emqx_authn">> => #{
+                   <<"authenticators">> => [AuthenticatorConfig]
+               }},
+    NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
+                                       #{nullable => true}),
+    #{emqx_authn := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig),
+    case emqx_authn:update_or_create_authenticator(?CHAIN, AuthenticatorID, NAuthenticatorConfig) of
+        {ok, Authenticator} ->
+            {200, Authenticator};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end;
+authenticators2(delete, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of
+        ok ->
+            {204};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end.
+
+position(post, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"position">> := Position} when is_integer(Position) ->
+            case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of
+                ok ->
+                    {204};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end;
+        _ ->
+            serialize_error({missing_parameter, position})
+    end.
+
+import_users(post, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"filename">> := Filename} when is_binary(Filename) ->
+            case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of
+                ok ->
+                    {204};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end;
+        _ ->
+            serialize_error({missing_parameter, filename})
+    end.
+
+users(post, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"user_id">> := _,
+          <<"password">> := _} = UserInfo ->
+            case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of
+                {ok, User} ->
+                    {201, User};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end;
+        _ ->
+            serialize_error({missing_parameter, user_id})
+    end;
+users(get, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    case emqx_authn:list_users(?CHAIN, AuthenticatorID) of
+        {ok, Users} ->
+            {200, Users};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end.
+
+users2(patch, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    UserID = cowboy_req:binding(user_id, Request),
+    {ok, Body, _} = cowboy_req:read_body(Request),
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"password">> := _} = UserInfo ->
+            case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of
+                {ok, User} ->
+                    {200, User};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end;
+        _ ->
+            serialize_error({missing_parameter, password})
+    end;
+users2(get, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    UserID = cowboy_req:binding(user_id, Request),
+    case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of
+        {ok, User} ->
+            {200, User};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end;
+users2(delete, Request) ->
+    AuthenticatorID = cowboy_req:binding(id, Request),
+    UserID = cowboy_req:binding(user_id, Request),
+    case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of
+        ok ->
+            {204};
+        {error, Reason} ->
+            serialize_error(Reason)
+    end.
+
+serialize_error({not_found, {authenticator, ID}}) ->
+    {404, #{code => <<"NOT_FOUND">>,
+            message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}};
+serialize_error(name_has_be_used) ->
+    {409, #{code => <<"ALREADY_EXISTS">>,
+            message => <<"Name has be used">>}};
+serialize_error(out_of_range) ->
+    {400, #{code => <<"OUT_OF_RANGE">>,
+            message => <<"Out of range">>}};
+serialize_error({missing_parameter, Name}) ->
+    {400, #{code => <<"MISSING_PARAMETER">>,
+            message => list_to_binary(
+                io_lib:format("The input parameter '~p' that is mandatory for processing this request is not supplied", [Name])
+            )}};
+serialize_error(_) ->
+    {400, #{code => <<"BAD_REQUEST">>,
+            message => <<"Todo">>}}.

+ 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.

+ 13 - 9
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}
     ].
@@ -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) ->

+ 29 - 23
apps/emqx_authn/src/simple_authn/emqx_authn_http.erl

@@ -26,8 +26,8 @@
         , validations/0
         ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -57,7 +57,9 @@ fields(post) ->
     ] ++ 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}
     , {form_data,       fun form_data/1}
     , {request_timeout, fun request_timeout/1}
@@ -105,37 +107,41 @@ request_timeout(_) -> undefined.
 %% APIs
 %%------------------------------------------------------------------------------
 
-create(ChainID, AuthenticatorName,
-        #{method := Method,
-          url := URL,
-          headers := Headers,
-          form_data := FormData,
-          request_timeout := RequestTimeout} = Config) ->
+create(#{ method := Method
+        , url := URL
+        , headers := Headers
+        , form_data := FormData
+        , request_timeout := RequestTimeout
+        , '_unique' := Unique
+        } = Config) ->
     #{path := Path,
       query := Query} = URIMap = parse_url(URL),
-    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},
-    ResourceID = <<ChainID/binary, "/", AuthenticatorName/binary>>,
-    case emqx_resource:create_local(ResourceID,
+    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 := _}, _) ->

+ 17 - 17
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -24,8 +24,8 @@
         , fields/1
         ]).
 
--export([ create/3
-        , update/4
+-export([ create/1
+        , update/2
         , authenticate/2
         , destroy/1
         ]).
@@ -48,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}}
@@ -82,6 +79,12 @@ fields(ssl_enable) ->
 fields(ssl_disable) ->
     [ {enable, #{type => false}} ].
 
+common_fields() ->
+    [ {name,            fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,       {enum, [jwt]}}
+    , {verify_claims,   fun verify_claims/1}
+    ].
+
 secret(type) -> string();
 secret(_) -> undefined.
 
@@ -129,18 +132,18 @@ verify_claims(_) -> 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
@@ -150,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 := _}, _) ->
@@ -181,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,

+ 20 - 14
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;

+ 29 - 18
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,7 +38,9 @@
 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,           fun salt_position/1}
     , {query,                   fun query/1}
@@ -70,34 +74,41 @@ 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 := _}, _) ->

+ 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 := _}, _) ->