Explorar el Código

Merge pull request #5478 from tigercl/feat/authn-hot-config

feat(authn): support superuser and hot config
tigercl hace 4 años
padre
commit
ee57df416a
Se han modificado 29 ficheros con 831 adiciones y 520 borrados
  1. 7 2
      apps/emqx/src/emqx_access_control.erl
  2. 11 8
      apps/emqx/src/emqx_channel.erl
  3. 2 2
      apps/emqx/src/emqx_config.erl
  4. 1 1
      apps/emqx/test/emqx_access_control_SUITE.erl
  5. 1 1
      apps/emqx/test/emqx_channel_SUITE.erl
  6. 3 3
      apps/emqx_authn/data/user-credentials.csv
  7. 4 2
      apps/emqx_authn/data/user-credentials.json
  8. 0 1
      apps/emqx_authn/include/emqx_authn.hrl
  9. 430 217
      apps/emqx_authn/src/emqx_authn.erl
  10. 135 92
      apps/emqx_authn/src/emqx_authn_api.erl
  11. 1 0
      apps/emqx_authn/src/emqx_authn_app.erl
  12. 8 1
      apps/emqx_authn/src/emqx_authn_sup.erl
  13. 60 28
      apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl
  14. 7 6
      apps/emqx_authn/src/simple_authn/emqx_authn_http.erl
  15. 7 2
      apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl
  16. 43 32
      apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl
  17. 7 1
      apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl
  18. 11 7
      apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl
  19. 0 58
      apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl
  20. 14 8
      apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl
  21. 9 3
      apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl
  22. 3 3
      apps/emqx_authn/test/data/user-credentials.csv
  23. 4 2
      apps/emqx_authn/test/data/user-credentials.json
  24. 15 7
      apps/emqx_authn/test/emqx_authn_SUITE.erl
  25. 18 12
      apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl
  26. 27 18
      apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl
  27. 1 1
      apps/emqx_gateway/src/emqx_gateway_ctx.erl
  28. 1 1
      apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl
  29. 1 1
      rebar.config

+ 7 - 2
apps/emqx/src/emqx_access_control.erl

@@ -27,9 +27,14 @@
 %%--------------------------------------------------------------------
 
 -spec(authenticate(emqx_types:clientinfo()) ->
-    ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
+    {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
 authenticate(Credential) ->
-    run_hooks('client.authenticate', [Credential], ok).
+    case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of
+        ok ->
+            {ok, #{superuser => false}};
+        Other ->
+            Other
+    end.
 
 %% @doc Check Authorization
 -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())

+ 11 - 8
apps/emqx/src/emqx_channel.erl

@@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti
             {error, ?RC_BAD_AUTHENTICATION_METHOD}
     end.
 
-do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
+do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) ->
     Properties = #{'Authentication-Method' => AuthMethod},
     case emqx_access_control:authenticate(Credential) of
-        ok ->
-            {ok, Properties, Channel#channel{auth_cache = #{}}};
-        {ok, AuthData} ->
+        {ok, Result} ->
+            {ok, Properties,
+             Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
+                             auth_cache = #{}}};
+        {ok, Result, AuthData} ->
             {ok, Properties#{'Authentication-Data' => AuthData},
-             Channel#channel{auth_cache = #{}}};
+             Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
+                             auth_cache = #{}}};
         {continue, AuthCache} ->
             {continue, Properties, Channel#channel{auth_cache = AuthCache}};
         {continue, AuthData, AuthCache} ->
@@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
             {error, emqx_reason_codes:connack_error(Reason)}
     end;
 
-do_authenticate(Credential, Channel) ->
+do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
     case emqx_access_control:authenticate(Credential) of
-        ok ->
-            {ok, #{}, Channel};
+        {ok, #{superuser := Superuser}} ->
+            {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}};
         {error, Reason} ->
             {error, emqx_reason_codes:connack_error(Reason)}
     end.

+ 2 - 2
apps/emqx/src/emqx_config.erl

@@ -101,9 +101,9 @@
 }.
 
 %% raw_config() is the config that is NOT parsed and tranlated by hocon schema
--type raw_config() :: #{binary() => term()} | undefined.
+-type raw_config() :: #{binary() => term()} | list() | undefined.
 %% config() is the config that is parsed and tranlated by hocon schema
--type config() :: #{atom() => term()} | undefined.
+-type config() :: #{atom() => term()} | list() | undefined.
 -type app_envs() :: [proplists:property()].
 
 %% @doc For the given path, get root value enclosed in a single-key map.

+ 1 - 1
apps/emqx/test/emqx_access_control_SUITE.erl

@@ -33,7 +33,7 @@ end_per_suite(_Config) ->
     emqx_ct_helpers:stop_apps([]).
 
 t_authenticate(_) ->
-    ?assertMatch(ok, emqx_access_control:authenticate(clientinfo())).
+    ?assertMatch({ok, _}, emqx_access_control:authenticate(clientinfo())).
 
 t_authorize(_) ->
     Publish = ?PUBLISH_PACKET(?QOS_0, <<"t">>, 1, <<"payload">>),

+ 1 - 1
apps/emqx/test/emqx_channel_SUITE.erl

@@ -181,7 +181,7 @@ init_per_suite(Config) ->
     %% Access Control Meck
     ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
     ok = meck:expect(emqx_access_control, authenticate,
-                     fun(_) -> ok end),
+                     fun(_) -> {ok, #{superuser => false}} end),
     ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end),
     %% Broker Meck
     ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),

+ 3 - 3
apps/emqx_authn/data/user-credentials.csv

@@ -1,3 +1,3 @@
-user_id,password_hash,salt
-myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235
-myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139
+user_id,password_hash,salt,superuser
+myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
+myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

+ 4 - 2
apps/emqx_authn/data/user-credentials.json

@@ -2,11 +2,13 @@
     {
         "user_id":"myuser1",
         "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
-        "salt": "e378187547bf2d6f0545a3f441aa4d8a"
+        "salt": "e378187547bf2d6f0545a3f441aa4d8a",
+        "superuser": true
     },
     {
         "user_id":"myuser2",
         "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
-        "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f"
+        "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
+        "superuser": false
     }
 ]

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

@@ -26,7 +26,6 @@
         { id :: binary()
         , name :: binary()
         , provider :: module()
-        , config :: map()
         , state :: map()
         }).
 

+ 430 - 217
apps/emqx_authn/src/emqx_authn.erl

@@ -16,7 +16,17 @@
 
 -module(emqx_authn).
 
+-behaviour(gen_server).
+
+-behaviour(emqx_config_handler).
+
 -include("emqx_authn.hrl").
+-include_lib("emqx/include/logger.hrl").
+
+-export([ pre_config_update/2
+        , post_config_update/3
+        , update_config/2
+        ]).
 
 -export([ enable/0
         , disable/0
@@ -25,6 +35,10 @@
 
 -export([authenticate/2]).
 
+-export([ start_link/0
+        , stop/0
+        ]).
+
 -export([ create_chain/1
         , delete_chain/1
         , lookup_chain/1
@@ -35,7 +49,7 @@
         , update_or_create_authenticator/3
         , lookup_authenticator/2
         , list_authenticators/1
-        , move_authenticator_to_the_nth/3
+        , move_authenticator/3
         ]).
 
 -export([ import_users/3
@@ -46,34 +60,138 @@
         , list_users/2
         ]).
 
--export([mnesia/1]).
-
--boot_mnesia({mnesia, [boot]}).
--copy_mnesia({mnesia, [copy]}).
+%% gen_server callbacks
+-export([ init/1
+        , handle_call/3
+        , handle_cast/2
+        , handle_info/2
+        , terminate/2
+        , code_change/3
+        ]).
 
 -define(CHAIN_TAB, emqx_authn_chain).
 
--rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}).
-
 %%------------------------------------------------------------------------------
-%% Mnesia bootstrap
+%% APIs
 %%------------------------------------------------------------------------------
 
-%% @doc Create or replicate tables.
--spec(mnesia(boot) -> ok).
-mnesia(boot) ->
-    %% Optimize storage
-    StoreProps = [{ets, [{read_concurrency, true}]}],
-    %% Chain table
-    ok = ekka_mnesia:create_table(?CHAIN_TAB, [
-                {ram_copies, [node()]},
-                {record_name, chain},
-                {local_content, true},
-                {attributes, record_info(fields, chain)},
-                {storage_properties, StoreProps}]);
-
-mnesia(copy) ->
-    ok = ekka_mnesia:copy_table(?CHAIN_TAB, ram_copies).
+pre_config_update({enable, Enable}, _OldConfig) ->
+    {ok, Enable};
+pre_config_update({create_authenticator, Config}, OldConfig) ->
+    {ok, OldConfig ++ [Config]};
+pre_config_update({delete_authenticator, ID}, OldConfig) ->
+    case lookup_authenticator(?CHAIN, ID) of
+        {error, Reason} -> {error, Reason};
+        {ok, #{name := Name}} ->
+            NewConfig = lists:filter(fun(#{<<"name">> := N}) ->
+                                         N =/= Name
+                                     end, OldConfig),
+            {ok, NewConfig}
+    end;
+pre_config_update({update_authenticator, ID, Config}, OldConfig) ->
+    case lookup_authenticator(?CHAIN, ID) of
+        {error, Reason} -> {error, Reason};
+        {ok, #{name := Name}} ->
+            NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
+                                      case N =:= Name of
+                                          true -> Config;
+                                          false -> C
+                                      end
+                                  end, OldConfig),
+            {ok, NewConfig}
+    end;
+pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) ->
+    case lookup_authenticator(?CHAIN, ID) of
+        {error, _Reason} -> OldConfig ++ [Config];
+        {ok, #{name := Name}} ->
+            NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
+                                      case N =:= Name of
+                                          true -> Config;
+                                          false -> C
+                                      end
+                                  end, OldConfig),
+            {ok, NewConfig}
+    end;
+pre_config_update({move_authenticator, ID, Position}, OldConfig) ->
+    case lookup_authenticator(?CHAIN, ID) of
+        {error, Reason} -> {error, Reason};
+        {ok, #{name := Name}} ->
+            {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig),
+            case Position of
+                <<"top">> ->
+                    {ok, [Found | Part1] ++ Part2};
+                <<"bottom">> ->
+                    {ok, Part1 ++ Part2 ++ [Found]};
+                Before ->
+                    case binary:split(Before, <<":">>, [global]) of
+                        [<<"before">>, ID0] ->
+                            case lookup_authenticator(?CHAIN, ID0) of
+                                {error, Reason} -> {error, Reason};
+                                {ok, #{name := Name1}} ->
+                                    {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2),
+                                    {ok, NPart1 ++ [Found, NFound | NPart2]}
+                            end;
+                        _ ->
+                            {error, {invalid_parameter, position}}
+                    end
+            end
+    end.
+
+post_config_update({enable, true}, _NewConfig, _OldConfig) ->
+    emqx_authn:enable();
+post_config_update({enable, false}, _NewConfig, _OldConfig) ->
+    emqx_authn:disable();
+post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
+    case lists:filter(
+             fun(#{name := N}) ->
+                 N =:= Name
+             end, NewConfig) of
+        [Config] ->
+            create_authenticator(?CHAIN, Config);
+        [_Config | _] ->
+            {error, name_has_be_used}
+    end;
+post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig) ->
+    case delete_authenticator(?CHAIN, ID) of
+        ok -> ok;
+        {error, Reason} -> throw(Reason)
+    end;
+post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
+    case lists:filter(
+             fun(#{name := N}) ->
+                 N =:= Name
+             end, NewConfig) of
+        [Config] ->
+            update_authenticator(?CHAIN, ID, Config);
+        [_Config | _] ->
+            {error, name_has_be_used}
+    end;
+post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig) ->
+    case lists:filter(
+             fun(#{name := N}) ->
+                 N =:= Name
+             end, NewConfig) of
+        [Config] ->
+            update_or_create_authenticator(?CHAIN, ID, Config);
+        [_Config | _] ->
+            {error, name_has_be_used}
+    end;
+post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig) ->
+    NPosition = case Position of
+                    <<"top">> -> top;
+                    <<"bottom">> -> bottom;
+                    Before ->
+                        case binary:split(Before, <<":">>, [global]) of
+                            [<<"before">>, ID0] ->
+                                {before, ID0};
+                            _ ->
+                                {error, {invalid_parameter, position}}
+                        end
+                end,
+    move_authenticator(?CHAIN, ID, NPosition).
+
+update_config(Path, ConfigRequest) ->
+    emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
 
 enable() ->
     case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
@@ -94,7 +212,7 @@ is_enabled() ->
               end, Callbacks).
 
 authenticate(Credential, _AuthResult) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of
+    case ets:lookup(?CHAIN_TAB, ?CHAIN) of
         [#chain{authenticators = Authenticators}] ->
             do_authenticate(Authenticators, Credential);
         [] ->
@@ -108,162 +226,48 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo
         ignore ->
             do_authenticate(More, Credential);
         Result ->
-            %% ok
-            %% {ok, AuthData}
+            %% {ok, Extra}
+            %% {ok, Extra, AuthData}
+            %% {ok, MetaData}
             %% {continue, AuthCache}
             %% {continue, AuthData, AuthCache}
             %% {error, Reason}
             {stop, Result}
     end.
 
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+    gen_server:stop(?MODULE).
+
 create_chain(#{id := ID}) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ID, write) of
-                [] ->
-                    Chain = #chain{id = ID,
-                                   authenticators = [],
-                                   created_at = erlang:system_time(millisecond)},
-                    mnesia:write(?CHAIN_TAB, Chain, write),
-                    {ok, serialize_chain(Chain)};
-                [_ | _] ->
-                    {error, {already_exists, {chain, ID}}}
-            end
-        end).
+    gen_server:call(?MODULE, {create_chain, ID}).
 
 delete_chain(ID) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ID, write) of
-                [] ->
-                    {error, {not_found, {chain, ID}}};
-                [#chain{authenticators = Authenticators}] ->
-                    _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
-                    mnesia:delete(?CHAIN_TAB, ID, write)
-            end
-        end).
+    gen_server:call(?MODULE, {delete_chain, ID}).
 
 lookup_chain(ID) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ID) of
-        [] ->
-            {error, {not_found, {chain, ID}}};
-        [Chain] ->
-            {ok, serialize_chain(Chain)}
-    end.
+    gen_server:call(?MODULE, {lookup_chain, ID}).
 
 list_chains() ->
     Chains = ets:tab2list(?CHAIN_TAB),
     {ok, [serialize_chain(Chain) || Chain <- Chains]}.
 
-create_authenticator(ChainID, #{name := Name} = Config) ->
-    UpdateFun =
-        fun(Chain = #chain{authenticators = Authenticators}) ->
-            case lists:keymember(Name, 2, Authenticators) of
-                true ->
-                    {error, name_has_be_used};
-                false ->
-                    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}
-                    end
-            end
-        end,
-    update_chain(ChainID, UpdateFun).
+create_authenticator(ChainID, Config) ->
+    gen_server:call(?MODULE, {create_authenticator, ChainID, Config}).
 
 delete_authenticator(ChainID, AuthenticatorID) ->
-    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case lists:keytake(AuthenticatorID, 1, Authenticators) of
-                        false ->
-                            {error, {not_found, {authenticator, AuthenticatorID}}};
-                        {value, {_, _, Authenticator}, NAuthenticators} ->
-                            _ = do_delete_authenticator(Authenticator),
-                            NChain = Chain#chain{authenticators = NAuthenticators},
-                            mnesia:write(?CHAIN_TAB, NChain, write)
-                    end
-                end,
-    update_chain(ChainID, UpdateFun).
+    gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}).
 
 update_authenticator(ChainID, AuthenticatorID, Config) ->
-    do_update_authenticator(ChainID, AuthenticatorID, Config, false).
+    gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}).
 
 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 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,
-    update_chain(ChainID, UpdateFun).
+    gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}).
 
 lookup_authenticator(ChainID, AuthenticatorID) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+    case ets:lookup(?CHAIN_TAB, ChainID) of
         [] ->
             {error, {not_found, {chain, ChainID}}};
         [#chain{authenticators = Authenticators}] ->
@@ -276,42 +280,180 @@ lookup_authenticator(ChainID, AuthenticatorID) ->
     end.
 
 list_authenticators(ChainID) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+    case ets:lookup(?CHAIN_TAB, ChainID) of
         [] ->
             {error, {not_found, {chain, ChainID}}};
         [#chain{authenticators = Authenticators}] ->
             {ok, serialize_authenticators(Authenticators)}
     end.
 
-move_authenticator_to_the_nth(ChainID, AuthenticatorID, N) ->
-    UpdateFun = fun(Chain = #chain{authenticators = Authenticators}) ->
-                    case move_authenticator_to_the_nth_(AuthenticatorID, Authenticators, N) 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(ChainID, AuthenticatorID, Position) ->
+    gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}).
 
 import_users(ChainID, AuthenticatorID, Filename) ->
-    call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]).
+    gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}).
 
 add_user(ChainID, AuthenticatorID, UserInfo) ->
-    call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]).
+    gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}).
 
 delete_user(ChainID, AuthenticatorID, UserID) ->
-    call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]).
+    gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}).
 
 update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
-    call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]).
+    gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}).
 
 lookup_user(ChainID, AuthenticatorID, UserID) ->
-    call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]).
+    gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}).
 
+%% TODO: Support pagination
 list_users(ChainID, AuthenticatorID) ->
-    call_authenticator(ChainID, AuthenticatorID, list_users, []).
+    gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}).
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks
+%%--------------------------------------------------------------------
+
+init(_Opts) ->
+    _ = ets:new(?CHAIN_TAB, [ named_table, set, public
+                            , {keypos, #chain.id}
+                            , {read_concurrency, true}]),
+    {ok, #{}}.
+
+handle_call({create_chain, ID}, _From, State) ->
+    case ets:member(?CHAIN_TAB, ID) of
+        true ->
+            reply({error, {already_exists, {chain, ID}}}, State);
+        false ->
+            Chain = #chain{id = ID,
+                           authenticators = [],
+                           created_at = erlang:system_time(millisecond)},
+            true = ets:insert(?CHAIN_TAB, Chain),
+            reply({ok, serialize_chain(Chain)}, State)
+    end;
+
+handle_call({delete_chain, ID}, _From, State) ->
+    case ets:lookup(?CHAIN_TAB, ID) of
+        [] ->
+            reply({error, {not_found, {chain, ID}}}, State);
+        [#chain{authenticators = Authenticators}] ->
+            _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
+            true = ets:delete(?CHAIN_TAB, ID),
+            reply(ok, State)
+    end;
+
+handle_call({lookup_chain, ID}, _From, State) ->
+    case ets:lookup(?CHAIN_TAB, ID) of
+        [] ->
+            reply({error, {not_found, {chain, ID}}}, State);
+        [Chain] ->
+            reply({ok, serialize_chain(Chain)}, State)
+    end;
+
+handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case lists:keymember(Name, 2, Authenticators) of
+                true ->
+                    {error, name_has_be_used};
+                false ->
+                    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}],
+                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
+                            {ok, serialize_authenticator(Authenticator)};
+                        {error, Reason} ->
+                            {error, Reason}
+                    end
+            end
+        end,
+    Reply = update_chain(ChainID, UpdateFun),
+    reply(Reply, State);
+
+handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case lists:keytake(AuthenticatorID, 1, Authenticators) of
+                false ->
+                    {error, {not_found, {authenticator, AuthenticatorID}}};
+                {value, {_, _, Authenticator}, NAuthenticators} ->
+                    _ = do_delete_authenticator(Authenticator),
+                    true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
+                    ok
+            end
+        end,
+    Reply = update_chain(ChainID, UpdateFun),
+    reply(Reply, State);
+
+handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
+    Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false),
+    reply(Reply, State);
+
+handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
+    Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true),
+    reply(Reply, State);
+
+handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
+                {ok, NAuthenticators} ->
+                    true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
+                    ok;
+                {error, Reason} ->
+                    {error, Reason}
+            end
+        end,
+    Reply = update_chain(ChainID, UpdateFun),
+    reply(Reply, State);
+
+handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]),
+    reply(Reply, State);
+
+handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]),
+    reply(Reply, State);
+
+handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]),
+    reply(Reply, State);
+
+handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]),
+    reply(Reply, State);
+
+handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]),
+    reply(Reply, State);
+
+handle_call({list_users, ChainID, AuthenticatorID}, _From, State) ->
+    Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []),
+    reply(Reply, State);
+
+handle_call(Req, _From, State) ->
+    ?LOG(error, "Unexpected call: ~p", [Req]),
+    {reply, ignored, State}.
+
+handle_cast(Req, State) ->
+    ?LOG(error, "Unexpected case: ~p", [Req]),
+    {noreply, State}.
+
+handle_info(Info, State) ->
+    ?LOG(error, "Unexpected info: ~p", [Info]),
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+reply(Reply, State) ->
+    {reply, Reply, State}.
 
 %%------------------------------------------------------------------------------
 %% Internal functions
@@ -348,6 +490,21 @@ switch_version(State = #{version := ?VER_2}) ->
 switch_version(State) ->
     State#{version => ?VER_1}.
 
+split_by_name(Name, Config) ->
+    {Part1, Part2, true} = lists:foldl(
+             fun(#{<<"name">> := N} = C, {P1, P2, F0}) ->
+                 F = case N =:= Name of
+                         true -> true;
+                         false -> F0
+                     end,
+                 case F of
+                     false -> {[C | P1], P2, F};
+                     true -> {P1, [C | P2], F}
+                 end
+             end, {[], [], false}, Config),
+    [Found | NPart2] = lists:reverse(Part2),
+    {ok, Found, lists:reverse(Part1), NPart2}.
+
 do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
     Provider = authenticator_provider(Config),
     Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
@@ -356,7 +513,6 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
             Authenticator = #authenticator{id = AuthenticatorID,
                                            name = Name,
                                            provider = Provider,
-                                           config = Config,
                                            state = switch_version(State)},
             {ok, Authenticator};
         {error, Reason} ->
@@ -366,44 +522,107 @@ do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
 do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
     _ = Provider:destroy(State),
     ok.
+
+update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
+    UpdateFun = 
+        fun(#chain{authenticators = Authenticators} = Chain) ->
+            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}],
+                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
+                                            {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,
+                                                                                           state = switch_version(NewState)},
+                                            NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
+                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
+                                            {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,
+                                                                                           state = switch_version(NewState)},
+                                            NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
+                                            true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
+                                            _ = Provider:destroy(State),
+                                            {ok, serialize_authenticator(NewAuthenticator)};
+                                        {error, Reason} ->
+                                            {error, Reason}
+                                    end
+                            end
+                    end
+            end
+        end,
+    update_chain(ChainID, UpdateFun).
     
 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_(AuthenticatorID, Authenticators, N, []);
-move_authenticator_to_the_nth_(_, _, _) ->
-    {error, out_of_range}.
-
-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_(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_(AuthenticatorID, [Authenticator | More], N, Passed) ->
-    move_authenticator_to_the_nth_(AuthenticatorID, More, N, [Authenticator | Passed]).
+do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) ->
+    case lists:keytake(AuthenticatorID, 1, Authenticators) of
+        false ->
+            {error, {not_found, {authenticator, AuthenticatorID}}};
+        {value, Authenticator, NAuthenticators} ->
+            do_move_authenticator(Authenticator, NAuthenticators, Position)
+    end;
+
+do_move_authenticator(Authenticator, Authenticators, top) ->
+    {ok, [Authenticator | Authenticators]};
+do_move_authenticator(Authenticator, Authenticators, bottom) ->
+    {ok, Authenticators ++ [Authenticator]};
+do_move_authenticator(Authenticator, Authenticators, {before, ID}) ->
+    insert(Authenticator, Authenticators, ID, []).
+
+insert(_, [], ID, _) ->
+    {error, {not_found, {authenticator, ID}}};
+insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) ->
+    {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
+insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) ->
+    insert(Authenticator, More, ID, [Authenticator0 | Acc]).
 
 update_chain(ChainID, UpdateFun) ->
-    trans(
-        fun() ->
-            case mnesia:read(?CHAIN_TAB, ChainID, write) of
-                [] ->
-                    {error, {not_found, {chain, ChainID}}};
-                [Chain] ->
-                    UpdateFun(Chain)
-            end
-        end).
-
-call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
-    case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
+    case ets:lookup(?CHAIN_TAB, ChainID) of
         [] ->
             {error, {not_found, {chain, ChainID}}};
-        [#chain{authenticators = Authenticators}] ->
+        [Chain] ->
+            UpdateFun(Chain)
+    end.
+
+call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
+    UpdateFun =
+        fun(#chain{authenticators = Authenticators}) ->
             case lists:keyfind(AuthenticatorID, 1, Authenticators) of
                 false ->
                     {error, {not_found, {authenticator, AuthenticatorID}}};
@@ -415,7 +634,8 @@ call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
                             {error, unsupported_feature}
                     end
             end
-    end.
+        end,
+    update_chain(ChainID, UpdateFun).
 
 serialize_chain(#chain{id = ID,
                        authenticators = Authenticators,
@@ -428,14 +648,7 @@ serialize_authenticators(Authenticators) ->
     [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
 
 serialize_authenticator(#authenticator{id = ID,
-                                       config = Config}) ->
-    Config#{id => ID}.
-
-trans(Fun) ->
-    trans(Fun, []).
-
-trans(Fun, Args) ->
-    case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of
-        {atomic, Res} -> Res;
-        {aborted, Reason} -> {error, Reason}
-    end.
+                                       name = Name,
+                                       provider = Provider,
+                                       state = State}) ->
+    #{id => ID, name => Name, provider => Provider, state => State}.

+ 135 - 92
apps/emqx_authn/src/emqx_authn_api.erl

@@ -24,7 +24,7 @@
         , authentication/2
         , authenticators/2
         , authenticators2/2
-        , position/2
+        , move/2
         , import_users/2
         , users/2
         , users2/2
@@ -109,7 +109,7 @@ api_spec() ->
     {[ authentication_api()
      , authenticators_api()
      , authenticators_api2()
-     , position_api()
+     , move_api()
      , import_users_api()
      , users_api()
      , users2_api()
@@ -405,10 +405,10 @@ authenticators_api2() ->
     },
     {"/authentication/authenticators/:id", Metadata, authenticators2}.
 
-position_api() ->
+move_api() ->
     Metadata = #{
         post => #{
-            description => "Change the order of authenticators",
+            description => "Move authenticator",
             parameters => [
                 #{
                     name => id,
@@ -423,14 +423,30 @@ position_api() ->
                 content => #{
                     'application/json' => #{
                         schema => #{
-                            type => object,
-                            required => [position],
-                            properties => #{
-                                position => #{
-                                    type => integer,
-                                    example => 1
+                            oneOf => [
+                                #{
+                                    type => object,
+                                    required => [position],
+                                    properties => #{
+                                        position => #{
+                                            type => string,
+                                            enum => [<<"top">>, <<"bottom">>],
+                                            example => <<"top">>
+                                        }
+                                    }
+                                },
+                                #{
+                                    type => object,
+                                    required => [position],
+                                    properties => #{
+                                        position => #{
+                                            type => string,
+                                            description => <<"before:<authenticator_id>">>,
+                                            example => <<"before:67e4c9d3">>
+                                        }
+                                    }
                                 }
-                            }
+                            ]
                         }
                     }
                 }
@@ -444,7 +460,7 @@ position_api() ->
             }
         }
     },
-    {"/authentication/authenticators/:id/position", Metadata, position}.
+    {"/authentication/authenticators/:id/move", Metadata, move}.
 
 import_users_api() ->
     Metadata = #{
@@ -512,6 +528,10 @@ users_api() ->
                                 },
                                 password => #{
                                     type => string
+                                },
+                                superuser => #{
+                                    type => boolean,
+                                    default => false
                                 }
                             }
                         }
@@ -525,10 +545,12 @@ users_api() ->
                         'application/json' => #{
                             schema => #{
                                 type => object,
-                                required => [user_id],
                                 properties => #{
                                     user_id => #{
                                         type => string
+                                    },
+                                    superuser => #{
+                                        type => boolean
                                     }
                                 }
                             }
@@ -560,10 +582,12 @@ users_api() ->
                                 type => array,
                                 items => #{
                                     type => object,
-                                    required => [user_id],
                                     properties => #{
                                         user_id => #{
                                             type => string
+                                        },
+                                        superuser => #{
+                                            type => boolean
                                         }
                                     }
                                 }
@@ -604,10 +628,12 @@ users2_api() ->
                     'application/json' => #{
                         schema => #{
                             type => object,
-                            required => [password],
                             properties => #{
                                 password => #{
                                     type => string
+                                },
+                                superuser => #{
+                                    type => boolean    
                                 }
                             }
                         }
@@ -626,6 +652,9 @@ users2_api() ->
                                     properties => #{
                                         user_id => #{
                                             type => string
+                                        },
+                                        superuser => #{
+                                            type => boolean
                                         }
                                     }
                                 }
@@ -669,6 +698,9 @@ users2_api() ->
                                     properties => #{
                                         user_id => #{
                                             type => string
+                                        },
+                                        superuser => #{
+                                            type => boolean
                                         }
                                     }
                                 }
@@ -758,6 +790,7 @@ definitions() ->
                          , minirest:ref(<<"password_based_mysql">>)
                          , minirest:ref(<<"password_based_pgsql">>)
                          , minirest:ref(<<"password_based_mongodb">>)
+                         , minirest:ref(<<"password_based_redis">>)
                          , minirest:ref(<<"password_based_http_server">>)
                          ]    
             }
@@ -1054,7 +1087,13 @@ definitions() ->
 
     PasswordBasedRedisDef = #{
         type => object,
-        required => [],
+        required => [ server_type
+                    , server
+                    , servers
+                    , password
+                    , database
+                    , query
+                    ],
         properties => #{
             server_type => #{
                 type => string,
@@ -1083,7 +1122,7 @@ definitions() ->
             },
             database => #{
                 type => integer,
-                exmaple => 0
+                example => 0
             },
             query => #{
                 type => string,
@@ -1253,14 +1292,9 @@ definitions() ->
 authentication(post, Request) ->
     {ok, Body, _} = cowboy_req:read_body(Request),
     case emqx_json:decode(Body, [return_maps]) of
-        #{<<"enable">> := true} ->
-            ok = emqx_authn:enable(),
-            {204};
-        #{<<"enable">> := false} ->
-            ok = emqx_authn:disable(),
+        #{<<"enable">> := Enable} ->
+            {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}),
             {204};
-        #{<<"enable">> := _} ->
-            serialize_error({invalid_parameter, enable});
         _ ->
             serialize_error({missing_parameter, enable})
     end;
@@ -1270,96 +1304,100 @@ authentication(get, _Request) ->
 
 authenticators(post, Request) ->
     {ok, Body, _} = cowboy_req:read_body(Request),
-    AuthenticatorConfig = emqx_json:decode(Body, [return_maps]),
-    Config = #{<<"authentication">> => #{
-                   <<"authenticators">> => [AuthenticatorConfig]
-               }},
-    NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
-                                       #{nullable => true}),
-    #{authentication := #{authenticators := [NAuthenticatorConfig]}} = emqx_map_lib:unsafe_atom_key_map(NConfig),
-    case emqx_authn:create_authenticator(?CHAIN, NAuthenticatorConfig) of
-        {ok, Authenticator2} ->
-            {201, Authenticator2};
-        {error, Reason} ->
+    Config = emqx_json:decode(Body, [return_maps]),
+    case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of
+        {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}},
+               raw_config := RawConfig}} ->
+            [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
+            {200, RawConfig1#{id => ID}};
+        {error, {_, _, Reason}} ->
             serialize_error(Reason)
     end;
 authenticators(get, _Request) ->
+    RawConfig = get_raw_config([authentication, authenticators]),
     {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN),
-    {200, Authenticators}.
+    NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) ->
+                                        Config#{id => ID}
+                                    end, RawConfig, Authenticators),
+    {200, NAuthenticators}.
 
 authenticators2(get, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
     case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of
-        {ok, Authenticator} ->
-           {200, Authenticator};
+        {ok, #{id := ID, name := Name}} ->
+            RawConfig = get_raw_config([authentication, authenticators]),
+            [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
+            {200, RawConfig1#{id => ID}};
         {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 = #{<<"authentication">> => #{
-                   <<"authenticators">> => [AuthenticatorConfig]
-               }},
-    NConfig = hocon_schema:check_plain(emqx_authn_schema, Config,
-                                       #{nullable => true}),
-    #{authentication := #{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} ->
+    Config = emqx_json:decode(Body, [return_maps]),
+    case emqx_authn:update_config([authentication, authenticators],
+                                  {update_or_create_authenticator, AuthenticatorID, Config}) of
+        {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}},
+               raw_config := RawConfig}} ->
+            [RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name],
+            {200, RawConfig0#{id => ID}};
+        {error, {_, _, Reason}} ->
             serialize_error(Reason)
     end;
 authenticators2(delete, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
-    case emqx_authn:delete_authenticator(?CHAIN, AuthenticatorID) of
-        ok ->
+    case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of
+        {ok, _} ->
             {204};
-        {error, Reason} ->
+        {error, {_, _, Reason}} ->
             serialize_error(Reason)
     end.
 
-position(post, Request) ->
+move(post, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
     {ok, Body, _} = cowboy_req:read_body(Request),
-    NBody = emqx_json:decode(Body, [return_maps]),
-    Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody},
-                                      #{nullable => true}, ["position"]),
-    #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config),
-    case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of
-        ok ->
-            {204};
-        {error, Reason} ->
-            serialize_error(Reason)
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"position">> := Position} ->
+            case emqx_authn:update_config([authentication, authenticators], {move_authenticator, 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),
-    NBody = emqx_json:decode(Body, [return_maps]),
-    Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody},
-                                      #{nullable => true}, ["filename"]),
-    #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config),
-    case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of
-        ok ->
-            {204};
-        {error, Reason} ->
-            serialize_error(Reason)
+    case emqx_json:decode(Body, [return_maps]) of
+        #{<<"filename">> := 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),
-    NBody = emqx_json:decode(Body, [return_maps]),
-    Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody},
-                                      #{nullable => true}, ["user_info"]),
-    #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config),
-    case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of
-        {ok, User} ->
-            {201, User};
-        {error, Reason} ->
-            serialize_error(Reason)
+    case emqx_json:decode(Body, [return_maps]) of
+        #{ <<"user_id">> := UserID
+         , <<"password">> := Password} = UserInfo ->
+            Superuser = maps:get(<<"superuser">>, UserInfo, false),
+            case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID
+                                                               , password => Password
+                                                               , superuser => Superuser}) of
+                {ok, User} ->
+                    {201, User};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end;
+        #{<<"user_id">> := _} ->
+            serialize_error({missing_parameter, password});
+        _ ->
+            serialize_error({missing_parameter, user_id})
     end;
 users(get, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
@@ -1374,15 +1412,18 @@ users2(patch, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
     UserID = cowboy_req:binding(user_id, Request),
     {ok, Body, _} = cowboy_req:read_body(Request),
-    NBody = emqx_json:decode(Body, [return_maps]),
-    Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody},
-                                      #{nullable => true}, ["new_user_info"]),
-    #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config),
-    case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of
-        {ok, User} ->
-            {200, User};
-        {error, Reason} ->
-            serialize_error(Reason)
+    UserInfo = emqx_json:decode(Body, [return_maps]),
+    NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo),
+    case NUserInfo =:= #{} of
+        true ->
+            serialize_error({missing_parameter, password});
+        false ->
+            case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of
+                {ok, User} ->
+                    {200, User};
+                {error, Reason} ->
+                    serialize_error(Reason)
+            end
     end;
 users2(get, Request) ->
     AuthenticatorID = cowboy_req:binding(id, Request),
@@ -1403,15 +1444,17 @@ users2(delete, Request) ->
             serialize_error(Reason)
     end.
 
+get_raw_config(ConfKeyPath) ->
+    %% TODO: call emqx_config:get_raw(ConfKeyPath) directly
+    NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath],
+    emqx_map_lib:deep_get(NConfKeyPath, emqx_config:fill_defaults(emqx_config:get_raw([]))).
+
 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(

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

@@ -29,6 +29,7 @@
 start(_StartType, _StartArgs) ->
     ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
     {ok, Sup} = emqx_authn_sup:start_link(),
+    emqx_config_handler:add_handler([authentication, authenticators], emqx_authn),
     initialize(),
     {ok, Sup}.
 

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

@@ -26,4 +26,11 @@ start_link() ->
     supervisor:start_link({local, ?MODULE}, ?MODULE, []).
 
 init([]) ->
-    {ok, {{one_for_one, 10, 10}, []}}.
+    ChildSpecs = [
+        #{id => emqx_authn,
+          start => {emqx_authn, start_link, []},
+          restart => permanent,
+          type => worker,
+          modules => [emqx_authn]}
+    ],
+    {ok, {{one_for_one, 10, 10}, ChildSpecs}}.

+ 60 - 28
apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl

@@ -17,7 +17,6 @@
 -module(emqx_enhanced_authn_scram_mnesia).
 
 -include("emqx_authn.hrl").
--include_lib("esasl/include/esasl_scram.hrl").
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
@@ -48,6 +47,14 @@
 
 -rlog_shard({?AUTH_SHARD, ?TAB}).
 
+-record(user_info,
+        { user_id
+        , stored_key
+        , server_key
+        , salt
+        , superuser
+        }).
+
 %%------------------------------------------------------------------------------
 %% Mnesia bootstrap
 %%------------------------------------------------------------------------------
@@ -57,8 +64,8 @@
 mnesia(boot) ->
     ok = ekka_mnesia:create_table(?TAB, [
                 {disc_copies, [node()]},
-                {record_name, scram_user_credentail},
-                {attributes, record_info(fields, scram_user_credentail)},
+                {record_name, user_info},
+                {attributes, record_info(fields, user_info)},
                 {storage_properties, [{ets, [{read_concurrency, true}]}]}]);
 
 mnesia(copy) ->
@@ -126,20 +133,21 @@ authenticate(_Credential, _State) ->
 destroy(#{user_group := UserGroup}) ->
     trans(
         fun() ->
-            MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
-            ok = lists:foreach(fun(UserCredential) ->
-                                  mnesia:delete_object(?TAB, UserCredential, write)
+            MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}],
+            ok = lists:foreach(fun(UserInfo) ->
+                                  mnesia:delete_object(?TAB, UserInfo, write)
                                end, mnesia:select(?TAB, MatchSpec, write))
         end).
 
 add_user(#{user_id := UserID,
-           password := Password}, #{user_group := UserGroup} = State) ->
+           password := Password} = UserInfo, #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
             case mnesia:read(?TAB, {UserGroup, UserID}, write) of
                 [] ->
-                    add_user(UserID, Password, State),
-                    {ok, #{user_id => UserID}};
+                    Superuser = maps:get(superuser, UserInfo, false),
+                    add_user(UserID, Password, Superuser, State),
+                    {ok, #{user_id => UserID, superuser => Superuser}};
                 [_] ->
                     {error, already_exist}
             end
@@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) ->
             end
         end).
 
-update_user(UserID, #{password := Password},
+update_user(UserID, User,
             #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
             case mnesia:read(?TAB, {UserGroup, UserID}, write) of
                 [] ->
                     {error, not_found};
-                [_] ->
-                    add_user(UserID, Password, State),
-                    {ok, #{user_id => UserID}}
+                [#user_info{superuser = Superuser} = UserInfo] ->
+                    UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)},
+                    UserInfo2 = case maps:get(password, User, undefined) of
+                                    undefined ->
+                                        UserInfo1;
+                                    Password ->
+                                        {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
+                                        UserInfo1#user_info{stored_key = StoredKey,
+                                                            server_key = ServerKey,
+                                                            salt       = Salt}
+                                end,
+                    mnesia:write(?TAB, UserInfo2, write),
+                    {ok, serialize_user_info(UserInfo2)}
             end
         end).
 
 lookup_user(UserID, #{user_group := UserGroup}) ->
     case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
-        [#scram_user_credentail{user_id = {_, UserID}}] ->
-            {ok, #{user_id => UserID}};
+        [UserInfo] ->
+            {ok, serialize_user_info(UserInfo)};
         [] ->
             {error, not_found}
     end.
 
 %% TODO: Support Pagination
 list_users(#{user_group := UserGroup}) ->
-    Users = [#{user_id => UserID} ||
-                 #scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
+    Users = [serialize_user_info(UserInfo) ||
+                 #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
     {ok, Users}.
 
 %%------------------------------------------------------------------------------
@@ -195,13 +213,13 @@ ensure_auth_method(_, _) ->
     false.
 
 check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
-    LookupFun = fun(Username) ->
-                    lookup_user2(Username, State)
+    RetrieveFun = fun(Username) ->
+                    retrieve(Username, State)
                 end,
     case esasl_scram:check_client_first_message(
              Bin,
              #{iteration_count => IterationCount,
-               lookup => LookupFun}
+               retrieve => RetrieveFun}
          ) of
         {cotinue, ServerFirstMessage, Cache} ->
             {cotinue, ServerFirstMessage, Cache};
@@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
             {error, not_authorized}
     end.
 
-check_client_final_message(Bin, Cache, #{algorithm := Alg}) ->
+check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) ->
     case esasl_scram:check_client_final_message(
              Bin,
              Cache#{algorithm => Alg}
          ) of
         {ok, ServerFinalMessage} ->
-            {ok, ServerFinalMessage};
+            {ok, #{superuser => Superuser}, ServerFinalMessage};
         {error, _Reason} ->
             {error, not_authorized}
     end.
 
-add_user(UserID, Password, State) ->
-    UserCredential = esasl_scram:generate_user_credential(UserID, Password, State),
-    mnesia:write(?TAB, UserCredential, write).
+add_user(UserID, Password, Superuser, State) ->
+    {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
+    UserInfo = #user_info{user_id    = UserID,
+                          stored_key = StoredKey,
+                          server_key = ServerKey,
+                          salt       = Salt,
+                          superuser  = Superuser},
+    mnesia:write(?TAB, UserInfo, write).
 
-lookup_user2(UserID, #{user_group := UserGroup}) ->
+retrieve(UserID, #{user_group := UserGroup}) ->
     case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
-        [#scram_user_credentail{} = UserCredential] ->
-            {ok, UserCredential};
+        [#user_info{stored_key = StoredKey,
+                    server_key = ServerKey,
+                    salt       = Salt,
+                    superuser  = Superuser}] ->
+            {ok, #{stored_key => StoredKey,
+                   server_key => ServerKey,
+                   salt => Salt,
+                   superuser => Superuser}};
         [] ->
             {error, not_found}
     end.
@@ -241,3 +270,6 @@ trans(Fun, Args) ->
         {atomic, Res} -> Res;
         {aborted, Reason} -> {error, Reason}
     end.
+
+serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
+    #{user_id => UserID, superuser => Superuser}.

+ 7 - 6
apps/emqx_authn/src/simple_authn/emqx_authn_http.erl

@@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique,
     try
         Request = generate_request(Credential, State),
         case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
-            {ok, 204, _Headers} -> ok;
+            {ok, 204, _Headers} -> {ok, #{superuser => false}};
             {ok, 200, Headers, Body} ->
                 ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
                 case safely_parse_body(ContentType, Body) of
-                    {ok, _NBody} ->
+                    {ok, NBody} ->
                         %% TODO: Return by user property
-                        ok;
+                        {ok, #{superuser => maps:get(<<"superuser">>, NBody, false),
+                               user_property => NBody}};
                     {error, _Reason} ->
-                        ok
+                        {ok, #{superuser => false}}
                 end;
             {error, _Reason} ->
                 ignore
@@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) ->
     end.
 
 parse_body(<<"application/json">>, Body) ->
-    {ok, emqx_json:decode(Body)};
+    {ok, emqx_json:decode(Body, [return_maps])};
 parse_body(<<"application/x-www-form-urlencoded">>, Body) ->
-    {ok, cow_qs:parse_qs(Body)};
+    {ok, maps:from_list(cow_qs:parse_qs(Body))};
 parse_body(ContentType, _) ->
     {error, {unsupported_content_type, ContentType}}.

+ 7 - 2
apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl

@@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK,
            end,
     VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
     case verify(JWT, JWKs, VerifyClaims) of
-        ok -> ok;
+        {ok, Extra} -> {ok, Extra};
         {error, invalid_signature} -> ignore;
         {error, {claims, _}} -> {error, bad_username_or_password}
     end.
@@ -239,7 +239,12 @@ verify(JWS, [JWK | More], VerifyClaims) ->
     try jose_jws:verify(JWK, JWS) of
         {true, Payload, _JWS} ->
             Claims = emqx_json:decode(Payload, [return_maps]),
-            verify_claims(Claims, VerifyClaims);
+            case verify_claims(Claims, VerifyClaims) of
+                ok ->
+                    {ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}};
+                {error, Reason} ->
+                    {error, Reason}
+            end;
         {false, _, _} ->
             verify(JWS, More, VerifyClaims)
     catch

+ 43 - 32
apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl

@@ -46,6 +46,7 @@
         { user_id :: {user_group(), user_id()}
         , password_hash :: binary()
         , salt :: binary()
+        , superuser :: boolean()
         }).
 
 -reflect_type([ user_id_type/0 ]).
@@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential,
     case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
         [] ->
             ignore;
-        [#user_info{password_hash = PasswordHash, salt = Salt0}] ->
+        [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] ->
             Salt = case Algorithm of
                        bcrypt -> PasswordHash;
                        _ -> Salt0
                    end,
             case PasswordHash =:= hash(Algorithm, Password, Salt) of
-                true -> ok;
+                true -> {ok, #{superuser => Superuser}};
                 false -> {error, bad_username_or_password}
             end
     end.
@@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential,
 destroy(#{user_group := UserGroup}) ->
     trans(
         fun() ->
-            MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}],
+            MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
             ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write))
         end).
 
@@ -179,14 +180,16 @@ import_users(Filename0, State) ->
     end.
 
 add_user(#{user_id := UserID,
-           password := Password},
+           password := Password} = UserInfo,
          #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
             case mnesia:read(?TAB, {UserGroup, UserID}, write) of
                 [] ->
-                    add(UserID, Password, State),
-                    {ok, #{user_id => UserID}};
+                    {PasswordHash, Salt} = hash(Password, State),
+                    Superuser = maps:get(superuser, UserInfo, false),
+                    insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
+                    {ok, #{user_id => UserID, superuser => Superuser}};
                 [_] ->
                     {error, already_exist}
             end
@@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) ->
             end
         end).
 
-update_user(UserID, #{password := Password},
+update_user(UserID, UserInfo,
             #{user_group := UserGroup} = State) ->
     trans(
         fun() ->
             case mnesia:read(?TAB, {UserGroup, UserID}, write) of
                 [] ->
                     {error, not_found};
-                [_] ->
-                    add(UserID, Password, State),
-                    {ok, #{user_id => UserID}}
+                [#user_info{ password_hash = PasswordHash
+                           , salt = Salt
+                           , superuser = Superuser}] ->
+                    NSuperuser = maps:get(superuser, UserInfo, Superuser),
+                    {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
+                                                 undefined ->
+                                                     {PasswordHash, Salt};
+                                                 Password ->
+                                                     hash(Password, State)
+                                             end,
+                    insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
+                    {ok, #{user_id => UserID, superuser => NSuperuser}}
             end
         end).
 
 lookup_user(UserID, #{user_group := UserGroup}) ->
     case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
-        [#user_info{user_id = {_, UserID}}] ->
-            {ok, #{user_id => UserID}};
+        [UserInfo] ->
+            {ok, serialize_user_info(UserInfo)};
         [] ->
             {error, not_found}
     end.
 
 list_users(#{user_group := UserGroup}) ->
-    Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
+    Users = [serialize_user_info(UserInfo) || #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
     {ok, Users}.
 
 %%------------------------------------------------------------------------------
@@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID,
                      <<"password_hash">> := PasswordHash} = UserInfo | More])
   when is_binary(UserID) andalso is_binary(PasswordHash) ->
     Salt = maps:get(<<"salt">>, UserInfo, <<>>),
-    insert_user(UserGroup, UserID, PasswordHash, Salt),
+    Superuser = maps:get(<<"superuser">>, UserInfo, false),
+    insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
     import(UserGroup, More);
 import(_UserGroup, [_ | _More]) ->
     {error, bad_format}.
@@ -282,7 +295,8 @@ import(UserGroup, File, Seq) ->
                 {ok, #{user_id := UserID,
                        password_hash := PasswordHash} = UserInfo} ->
                     Salt = maps:get(salt, UserInfo, <<>>),
-                    insert_user(UserGroup, UserID, PasswordHash, Salt),
+                    Superuser = maps:get(superuser, UserInfo, false),
+                    insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
                     import(UserGroup, File, Seq);
                 {error, Reason} ->
                     {error, Reason}
@@ -307,8 +321,6 @@ get_csv_header(File) ->
 get_user_info_by_seq(Fields, Seq) ->
     get_user_info_by_seq(Fields, Seq, #{}).
 
-get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) ->
-    {ok, Acc};
 get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) ->
     {ok, Acc};
 get_user_info_by_seq(_, [], _) ->
@@ -319,19 +331,13 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc)
     get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash});
 get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) ->
     get_user_info_by_seq(More1, More2, Acc#{salt => Salt});
+get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) ->
+    get_user_info_by_seq(More1, More2, Acc#{superuser => true});
+get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) ->
+    get_user_info_by_seq(More1, More2, Acc#{superuser => false});
 get_user_info_by_seq(_, _, _) ->
     {error, bad_format}.
 
--compile({inline, [add/3]}).
-add(UserID, Password, #{user_group := UserGroup,
-                        password_hash_algorithm := Algorithm} = State) ->
-    Salt = gen_salt(State),
-    PasswordHash = hash(Algorithm, Password, Salt),
-    case Algorithm of
-        bcrypt -> insert_user(UserGroup, UserID, PasswordHash);
-        _ -> insert_user(UserGroup, UserID, PasswordHash, Salt)
-    end.
-
 gen_salt(#{password_hash_algorithm := plain}) ->
     <<>>;
 gen_salt(#{password_hash_algorithm := bcrypt,
@@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) ->
 hash(Algorithm, Password, Salt) ->
     emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
 
-insert_user(UserGroup, UserID, PasswordHash) ->
-    insert_user(UserGroup, UserID, PasswordHash, <<>>).
+hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
+    Salt = gen_salt(State),
+    PasswordHash = hash(Algorithm, Password, Salt),
+    {PasswordHash, Salt}.
 
-insert_user(UserGroup, UserID, PasswordHash, Salt) ->
+insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) ->
      UserInfo = #user_info{user_id = {UserGroup, UserID},
                            password_hash = PasswordHash,
-                           salt = Salt},
+                           salt = Salt,
+                           superuser = Superuser},
     mnesia:write(?TAB, UserInfo, write).
 
 delete_user2(UserInfo) ->
@@ -376,8 +385,10 @@ trans(Fun, Args) ->
         {aborted, Reason} -> {error, Reason}
     end.
 
-
 to_binary(B) when is_binary(B) ->
     B;
 to_binary(L) when is_list(L) ->
     iolist_to_binary(L).
+
+serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
+    #{user_id => UserID, superuser => Superuser}.

+ 7 - 1
apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

@@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential,
                 ignore;
             Doc ->
                 case check_password(Password, Doc, State) of
-                    ok -> ok;
+                    ok ->
+                        {ok, #{superuser => superuser(Doc, State)}};
                     {error, {cannot_find_password_hash_field, PasswordHashField}} ->
                         ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
                         {error, bad_username_or_password};
@@ -221,6 +222,11 @@ check_password(Password,
             end
     end.
 
+superuser(Doc, #{superuser_field := SuperuserField}) ->
+    maps:get(SuperuserField, Doc, false);
+superuser(_, _) ->
+    false.
+
 hash(Algorithm, Password, Salt, prefix) ->
     emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
 hash(Algorithm, Password, Salt, suffix) ->

+ 11 - 7
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential,
         case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
             {ok, _Columns, []} -> ignore;
             {ok, Columns, Rows} ->
-                %% TODO: Support superuser
                 Selected = maps:from_list(lists:zip(Columns, Rows)),
-                check_password(Password, Selected, State);
+                case check_password(Password, Selected, State) of
+                    ok ->
+                        {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
+                    {error, Reason} ->
+                        {error, Reason}
+                end;
             {error, _Reason} ->
                 ignore
         end
     catch
-        error:Reason ->
-            ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
+        error:Error ->
+            ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
             ignore
     end.
 
@@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) ->
 check_password(undefined, _Selected, _State) ->
     {error, bad_username_or_password};
 check_password(Password,
-               #{password_hash := Hash},
+               #{<<"password_hash">> := Hash},
                #{password_hash_algorithm := bcrypt}) ->
     case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
         true -> ok;
         false -> {error, bad_username_or_password}
     end;
 check_password(Password,
-               #{password_hash := Hash} = Selected,
+               #{<<"password_hash">> := Hash} = Selected,
                #{password_hash_algorithm := Algorithm,
                  salt_position := SaltPosition}) ->
-    Salt = maps:get(salt, Selected, <<>>),
+    Salt = maps:get(<<"salt">>, Selected, <<>>),
     case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
         true -> ok;
         false -> {error, bad_username_or_password}

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

@@ -1,58 +0,0 @@
-%%--------------------------------------------------------------------
-%% 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.
-

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

@@ -18,6 +18,7 @@
 
 -include("emqx_authn.hrl").
 -include_lib("emqx/include/logger.hrl").
+-include_lib("epgsql/include/epgsql.hrl").
 -include_lib("typerefl/include/types.hrl").
 
 -behaviour(hocon_schema).
@@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential,
         case emqx_resource:query(Unique, {sql, Query, Params}) of
             {ok, _Columns, []} -> ignore;
             {ok, Columns, Rows} ->
-                %% TODO: Support superuser
-                Selected = maps:from_list(lists:zip(Columns, Rows)),
-                check_password(Password, Selected, State);
+                NColumns = [Name || #column{name = Name} <- Columns],
+                Selected = maps:from_list(lists:zip(NColumns, Rows)),
+                case check_password(Password, Selected, State) of
+                    ok ->
+                        {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
+                    {error, Reason} ->
+                        {error, Reason}
+                end;
             {error, _Reason} ->
                 ignore
         end
     catch
-        error:Reason ->
-            ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
+        error:Error ->
+            ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
             ignore
     end.
 
@@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) ->
 check_password(undefined, _Selected, _State) ->
     {error, bad_username_or_password};
 check_password(Password,
-               #{password_hash := Hash},
+               #{<<"password_hash">> := Hash},
                #{password_hash_algorithm := bcrypt}) ->
     case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
         true -> ok;
         false -> {error, bad_username_or_password}
     end;
 check_password(Password,
-               #{password_hash := Hash} = Selected,
+               #{<<"password_hash">> := Hash} = Selected,
                #{password_hash_algorithm := Algorithm,
                  salt_position := SaltPosition}) ->
-    Salt = maps:get(salt, Selected, <<>>),
+    Salt = maps:get(<<"salt">>, Selected, <<>>),
     case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
         true -> ok;
         false -> {error, bad_username_or_password}

+ 9 - 3
apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl

@@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential,
         NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
         case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
             {ok, Values} ->
-                check_password(Password, merge(Fields, Values), State);
+                Selected = merge(Fields, Values),
+                case check_password(Password, Selected, State) of
+                   ok ->
+                       {ok, #{superuser => maps:get("superuser", Selected, false)}};
+                   {error, Reason} ->
+                       {error, Reason}
+                end;
             {error, Reason} ->
                 ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
                 ignore
@@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) ->
     check_fields(More, true);
 check_fields(["salt" | More], HasPassHash) ->
     check_fields(More, HasPassHash);
-% check_fields(["is_superuser" | More], HasPassHash) ->
-%     check_fields(More, HasPassHash);
+check_fields(["superuser" | More], HasPassHash) ->
+    check_fields(More, HasPassHash);
 check_fields([Field | _], _) ->
     error({unsupported_field, Field}).
     

+ 3 - 3
apps/emqx_authn/test/data/user-credentials.csv

@@ -1,3 +1,3 @@
-user_id,password_hash,salt
-myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235
-myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139
+user_id,password_hash,salt,superuser
+myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
+myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

+ 4 - 2
apps/emqx_authn/test/data/user-credentials.json

@@ -2,11 +2,13 @@
     {
         "user_id":"myuser1",
         "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
-        "salt": "e378187547bf2d6f0545a3f441aa4d8a"
+        "salt": "e378187547bf2d6f0545a3f441aa4d8a",
+        "superuser": true
     },
     {
         "user_id":"myuser2",
         "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
-        "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f"
+        "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
+        "superuser": false
     }
 ]

+ 15 - 7
apps/emqx_authn/test/emqx_authn_SUITE.erl

@@ -71,7 +71,7 @@ t_authenticator(_) ->
                              secret => <<"abcdef">>,
                              secret_base64_encoded => false,
                              verify_claims => []},
-    {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
+    {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
 
     ID2 = <<"random">>,
     ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
@@ -79,17 +79,25 @@ t_authenticator(_) ->
 
     AuthenticatorName2 = <<"myauthenticator2">>,
     AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
-    {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
+    {ok, #{name := AuthenticatorName2, id := ID2}} = ?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">>}),
+    {ok, #{name := AuthenticatorName2, id := ID2}} = ?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)),
+    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
     ?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:move_authenticator(?CHAIN, ID2, bottom)),
+    ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
+
+    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})),
+
+    ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
+
+    ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})),
+
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
     ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
@@ -100,7 +108,7 @@ t_authenticate(_) ->
                    listener => mqtt_tcp,
                    username => <<"myuser">>,
 			       password => <<"mypass">>},
-    ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)),
+    ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
     ?assertEqual(false, emqx_authn:is_enabled()),
     emqx_authn:enable(),
     ?assertEqual(true, emqx_authn:is_enabled()),

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

@@ -52,21 +52,27 @@ t_jwt_authenticator(_) ->
     JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
     ClientInfo = #{username => <<"myuser">>,
 			       password => JWS},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+
+    Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
+    JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
+    ClientInfo1 = #{username => <<"myuser">>,
+			        password => JWS1},
+    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
 
     BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
     ClientInfo2 = ClientInfo#{password => BadJWS},
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
+    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
 
     %% secret_base64_encoded
     Config2 = Config#{secret => base64:encode(<<"abcdef">>),
                       secret_base64_encoded => true},
     ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
 
     Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
     ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
     ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
 
     %% Expiration
@@ -74,39 +80,39 @@ t_jwt_authenticator(_) ->
                 , <<"exp">> => erlang:system_time(second) - 60},
     JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
     ClientInfo3 = ClientInfo#{password => JWS3},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
+    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
 
     Payload4 = #{ <<"username">> => <<"myuser">>
                 , <<"exp">> => erlang:system_time(second) + 60},
     JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
     ClientInfo4 = ClientInfo#{password => JWS4},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
 
     %% Issued At
     Payload5 = #{ <<"username">> => <<"myuser">>
                 , <<"iat">> => erlang:system_time(second) - 60},
     JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
     ClientInfo5 = ClientInfo#{password => JWS5},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
 
     Payload6 = #{ <<"username">> => <<"myuser">>
                 , <<"iat">> => erlang:system_time(second) + 60},
     JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
     ClientInfo6 = ClientInfo#{password => JWS6},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)),
+    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
 
     %% Not Before
     Payload7 = #{ <<"username">> => <<"myuser">>
                 , <<"nbf">> => erlang:system_time(second) - 60},
     JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
     ClientInfo7 = ClientInfo#{password => JWS7},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
 
     Payload8 = #{ <<"username">> => <<"myuser">>
                 , <<"nbf">> => erlang:system_time(second) + 60},
     JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
     ClientInfo8 = ClientInfo#{password => JWS8},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)),
+    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
 
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.
@@ -128,8 +134,8 @@ t_jwt_authenticator2(_) ->
     JWS = generate_jws('public-key', Payload, PrivateKey),
     ClientInfo = #{username => <<"myuser">>,
 			       password => JWS},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
+    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
 
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.

+ 27 - 18
apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl

@@ -50,33 +50,36 @@ t_mnesia_authenticator(_) ->
 
     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">>)),
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
 
     ClientInfo = #{zone => external,
                    username => <<"myuser">>,
 			       password => <<"mypass">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
     ?AUTH:enable(),
-    ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)),
+    ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
 
     ClientInfo2 = ClientInfo#{username => <<"baduser">>},
-    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
+    ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
     ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
 
     ClientInfo3 = ClientInfo#{password => <<"badpass">>},
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
+    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
     ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
 
     UserInfo2 = UserInfo#{password => <<"mypass2">>},
-    ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
     ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
+
+    ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
+    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
 
     ?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, ID, UserInfo)),
+    ?assertMatch({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)),
 
@@ -104,10 +107,16 @@ t_import(_) ->
 
     ClientInfo1 = #{username => <<"myuser1">>,
 			        password => <<"mypassword1">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
-    ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
+    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+
+    ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
+                               password => <<"mypassword2">>},
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
+
+    ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
                                password => <<"mypassword3">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
+    ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
+
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
     ok.
 
@@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) ->
     {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">>}},
+    ?assertMatch({ok, #{user_id := <<"myuser">>}},
                  ?AUTH:add_user(?CHAIN, ID1,
                                 #{user_id => <<"myuser">>,
                                   password => <<"mypass1">>})),
-    ?assertEqual({ok, #{user_id => <<"myclient">>}},
+    ?assertMatch({ok, #{user_id := <<"myclient">>}},
                  ?AUTH:add_user(?CHAIN, ID2,
                                 #{user_id => <<"myclient">>,
                                   password => <<"mypass2">>})),
@@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) ->
     ClientInfo1 = #{username => <<"myuser">>,
                     clientid => <<"myclient">>,
 			        password => <<"mypass1">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
-    ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
+    ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
 
-    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)),
+    ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
     ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
-    ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
+    ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
 
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
     ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),

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

@@ -73,7 +73,7 @@ authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) ->
                    chain_id => ChainId
                   },
     case emqx_access_control:authenticate(ClientInfo) of
-        ok ->
+        {ok, _} ->
             {ok, mountpoint(ClientInfo)};
         {error, Reason} ->
             {error, Reason}

+ 1 - 1
apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl

@@ -87,7 +87,7 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">>
     ClientInfo = clientinfo(Lwm2mState),
     _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
     case emqx_access_control:authenticate(ClientInfo) of
-        ok ->
+        {ok, _} ->
             _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
 
             %% FIXME:

+ 1 - 1
rebar.config

@@ -63,7 +63,7 @@
     , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}
     , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.11.1"}}}
     , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}}
-    , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.1.0"}}}
+    , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
     ]}.
 
 {xref_ignores,