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

Merge pull request #4453 from k32/fix-auth-mnesia

fix(emqx_auth_mnesia): add missing combinations of permissions
k32 5 лет назад
Родитель
Сommit
d54643e9e1

+ 1 - 0
apps/emqx_auth_mnesia/src/emqx_acl_mnesia.erl

@@ -31,6 +31,7 @@
 
 init() ->
     ok = ekka_mnesia:create_table(emqx_acl, [
+            {type, bag},
             {disc_copies, [node()]},
             {attributes, record_info(fields, emqx_acl)},
             {storage_properties, [{ets, [{read_concurrency, true}]}]}]),

+ 42 - 7
apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl

@@ -39,13 +39,24 @@
 -spec(add_acl(login() | all, emqx_topic:topic(), pub | sub | pubsub, allow | deny) ->
         ok | {error, any()}).
 add_acl(Login, Topic, Action, Access) ->
-    Acls = #?TABLE{
-              filter = {Login, Topic},
-              action = Action,
-              access = Access,
-              created_at = erlang:system_time(millisecond)
-             },
-    ret(mnesia:transaction(fun mnesia:write/1, [Acls])).
+    Filter = {Login, Topic},
+    Acl = #?TABLE{
+             filter = Filter,
+             action = Action,
+             access = Access,
+             created_at = erlang:system_time(millisecond)
+            },
+    ret(mnesia:transaction(
+          fun() ->
+                  OldRecords = mnesia:wread({?TABLE, Filter}),
+                  case Action of
+                      pubsub ->
+                          update_permission(pub, Acl, OldRecords),
+                          update_permission(sub, Acl, OldRecords);
+                      _ ->
+                          update_permission(Action, Acl, OldRecords)
+                  end
+          end)).
 
 %% @doc Lookup acl by login
 -spec(lookup_acl(login() | all) -> list()).
@@ -233,3 +244,27 @@ print_acl({all, Topic, Action, Access, _}) ->
         "Acl($all topic = ~p action = ~p access = ~p)~n",
         [Topic, Action, Access]
      ).
+
+update_permission(Action, Acl0, OldRecords) ->
+    Acl = Acl0 #?TABLE{action = Action},
+    maybe_delete_shadowed_records(Action, OldRecords),
+    mnesia:write(Acl).
+
+maybe_delete_shadowed_records(_, []) ->
+    ok;
+maybe_delete_shadowed_records(Action1, [Rec = #emqx_acl{action = Action2} | Rest]) ->
+    if Action1 =:= Action2 ->
+            ok = mnesia:delete_object(Rec);
+       Action2 =:= pubsub ->
+            %% Perform migration from the old data format on the
+            %% fly. This is needed only for the enterprise version,
+            %% delete this branch on 5.0
+            mnesia:delete_object(Rec),
+            mnesia:write(Rec#?TABLE{action = other_action(Action1)});
+       true ->
+            ok
+    end,
+    maybe_delete_shadowed_records(Action1, Rest).
+
+other_action(pub) -> sub;
+other_action(sub) -> pub.

+ 60 - 8
apps/emqx_auth_mnesia/test/emqx_acl_mnesia_SUITE.erl

@@ -86,11 +86,15 @@ t_management(_Config) ->
     ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/%u">>, sub, deny),
     ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/+">>, pub, allow),
     ok = emqx_acl_mnesia_cli:add_acl(all, <<"#">>, pubsub, deny),
+    %% Sleeps below are needed to hide the race condition between
+    %% mnesia and ets dirty select in check_acl, that make this test
+    %% flaky
+    timer:sleep(100),
 
     ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))),
     ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))),
-    ?assertEqual(1, length(emqx_acl_mnesia_cli:lookup_acl(all))),
-    ?assertEqual(5, length(emqx_acl_mnesia_cli:all_acls())),
+    ?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl(all))),
+    ?assertEqual(6, length(emqx_acl_mnesia_cli:all_acls())),
 
     User1 = #{zone => external, clientid => <<"test_clientid">>},
     User2 = #{zone => external, clientid => <<"no_exist">>, username => <<"test_username">>},
@@ -105,11 +109,55 @@ t_management(_Config) ->
     deny  = emqx_access_control:check_acl(User3, subscribe, <<"topic/A/B">>),
     deny  = emqx_access_control:check_acl(User3, publish,   <<"topic/A/B">>),
 
+    %% Test merging of pubsub capability:
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, deny),
+    timer:sleep(100),
+    deny  = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    deny  = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, allow),
+    timer:sleep(100),
+    deny  = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    allow = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, allow),
+    timer:sleep(100),
+    allow = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    allow = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny),
+    timer:sleep(100),
+    deny  = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    allow = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny),
+    timer:sleep(100),
+    deny  = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    deny  = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+
+    %% Test implicit migration of pubsub to pub and sub:
+    ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>),
+    ok = mnesia:dirty_write(#emqx_acl{
+                               filter = {{clientid, <<"test_clientid">>}, <<"topic/mix">>},
+                               action = pubsub,
+                               access = allow,
+                               created_at = erlang:system_time(millisecond)
+                              }),
+    timer:sleep(100),
+    allow = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    allow = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny),
+    timer:sleep(100),
+    allow = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    deny  = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+    ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny),
+    timer:sleep(100),
+    deny  = emqx_access_control:check_acl(User1, subscribe,   <<"topic/mix">>),
+    deny  = emqx_access_control:check_acl(User1, publish,     <<"topic/mix">>),
+
     ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>),
     ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>),
+    ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>),
     ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/%u">>),
     ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/+">>),
     ok = emqx_acl_mnesia_cli:remove_acl(all, <<"#">>),
+    timer:sleep(100),
 
     ?assertEqual([], emqx_acl_mnesia_cli:all_acls()).
 
@@ -124,6 +172,7 @@ t_acl_cli(_Config) ->
 
     ?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
 
+    emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "deny"]),
     emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "allow"]),
     R1 = emqx_ctl:format("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",
                          [<<"test_clientid">>, <<"topic/A">>, pub, allow]),
@@ -136,11 +185,14 @@ t_acl_cli(_Config) ->
     ?assertEqual([R2], emqx_acl_mnesia_cli:cli(["show", "username", "test_username"])),
     ?assertEqual([R2], emqx_acl_mnesia_cli:cli(["list", "username"])),
 
+    emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pub", "allow"]),
     emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pubsub", "deny"]),
-    ?assertMatch(["Acl($all topic = <<\"#\">> action = pubsub access = deny)\n"],
-                 emqx_acl_mnesia_cli:cli(["list", "_all"])
+    ?assertMatch(["",
+                  "Acl($all topic = <<\"#\">> action = pub access = deny)",
+                  "Acl($all topic = <<\"#\">> action = sub access = deny)"],
+                 lists:sort(string:split(emqx_acl_mnesia_cli:cli(["list", "_all"]), "\n", all))
                 ),
-    ?assertEqual(3, length(emqx_acl_mnesia_cli:cli(["list"]))),
+    ?assertEqual(4, length(emqx_acl_mnesia_cli:cli(["list"]))),
 
     emqx_acl_mnesia_cli:cli(["del", "clientid", "test_clientid", "topic/A"]),
     emqx_acl_mnesia_cli:cli(["del", "username", "test_username", "topic/B"]),
@@ -169,7 +221,7 @@ t_rest_api(_Config) ->
                 }],
     {ok, _} = request_http_rest_add([], Params1),
     {ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]),
-    ?assertMatch(3, length(get_http_data(Re1))),
+    ?assertMatch(4, length(get_http_data(Re1))),
     {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/A"]),
     {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/B"]),
     {ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/C"]),
@@ -193,7 +245,7 @@ t_rest_api(_Config) ->
                 }],
     {ok, _} = request_http_rest_add([], Params2),
     {ok, Re2} = request_http_rest_list(["username", "test_username"]),
-    ?assertMatch(3, length(get_http_data(Re2))),
+    ?assertMatch(4, length(get_http_data(Re2))),
     {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/A"]),
     {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/B"]),
     {ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/C"]),
@@ -214,7 +266,7 @@ t_rest_api(_Config) ->
                 }],
     {ok, _} = request_http_rest_add([], Params3),
     {ok, Re3} = request_http_rest_list(["$all"]),
-    ?assertMatch(3, length(get_http_data(Re3))),
+    ?assertMatch(4, length(get_http_data(Re3))),
     {ok, _} = request_http_rest_delete(["$all", "topic", "topic/A"]),
     {ok, _} = request_http_rest_delete(["$all", "topic", "topic/B"]),
     {ok, _} = request_http_rest_delete(["$all", "topic", "topic/C"]),

+ 1 - 1
apps/emqx_management/src/emqx_mgmt_api_data.erl

@@ -127,7 +127,7 @@ import(_Bindings, Params) ->
 
 do_import(Filename) ->
     FullFilename = filename:join([emqx:get_env(data_dir), Filename]),
-    emqx_mgmt_data_backup:import(FullFilename).
+    emqx_mgmt_data_backup:import(FullFilename, "{}").
 
 download(#{filename := Filename}, _Params) ->
     FullFilename = filename:join([emqx:get_env(data_dir), Filename]),

+ 6 - 3
apps/emqx_management/src/emqx_mgmt_cli.erl

@@ -562,7 +562,9 @@ data(["export"]) ->
     end;
 
 data(["import", Filename]) ->
-    case emqx_mgmt_data_backup:import(Filename) of
+    data(["import", Filename, "--env", "{}"]);
+data(["import", Filename, "--env", Env]) ->
+    case emqx_mgmt_data_backup:import(Filename, Env) of
         ok ->
             emqx_ctl:print("The emqx data has been imported successfully.~n");
         {error, import_failed} ->
@@ -574,8 +576,9 @@ data(["import", Filename]) ->
     end;
 
 data(_) ->
-    emqx_ctl:usage([{"data import <File>",   "Import data from the specified file"},
-                    {"data export",          "Export data"}]).
+    emqx_ctl:usage([{"data import <File> [--env '<json>']",
+                     "Import data from the specified file, possibly with overrides"},
+                    {"data export", "Export data"}]).
 
 %%--------------------------------------------------------------------
 %% @doc acl Command

+ 50 - 8
apps/emqx_management/src/emqx_mgmt_data_backup.erl

@@ -52,7 +52,7 @@
         ]).
 
 -export([ export/0
-        , import/1
+        , import/2
         ]).
 
 %%--------------------------------------------------------------------
@@ -441,9 +441,11 @@ import_acl_mnesia(Acls, _) ->
     do_import_acl_mnesia(Acls).
 -else.
 import_auth_mnesia(Auths, FromVersion) when FromVersion =:= "4.0" orelse
-                                            FromVersion =:= "4.1" orelse
-                                            FromVersion =:= "4.2" ->
+                                            FromVersion =:= "4.1" ->
     do_import_auth_mnesia_by_old_data(Auths);
+import_auth_mnesia(Auths, "4.2") ->
+    %% 4.2 contains a bug where password is not base64-encoded
+    do_import_auth_mnesia_4_2(Auths);
 import_auth_mnesia(Auths, _) ->
     do_import_auth_mnesia(Auths).
 
@@ -454,6 +456,17 @@ import_acl_mnesia(Acls, FromVersion) when FromVersion =:= "4.0" orelse
 
 import_acl_mnesia(Acls, _) ->
     do_import_acl_mnesia(Acls).
+
+do_import_auth_mnesia_4_2(Auths) ->
+    case ets:info(emqx_user) of
+        undefined -> ok;
+        _ ->
+            CreatedAt = erlang:system_time(millisecond),
+            lists:foreach(fun(#{<<"login">> := Login,
+                                <<"password">> := Password}) ->
+                            mnesia:dirty_write({emqx_user, {get_old_type(), Login}, Password, CreatedAt})
+                          end, Auths)
+    end.
 -endif.
 
 do_import_auth_mnesia_by_old_data(Auths) ->
@@ -463,9 +476,11 @@ do_import_auth_mnesia_by_old_data(Auths) ->
             CreatedAt = erlang:system_time(millisecond),
             lists:foreach(fun(#{<<"login">> := Login,
                                 <<"password">> := Password}) ->
-                            mnesia:dirty_write({emqx_user, {username, Login}, base64:decode(Password), CreatedAt})
+                            mnesia:dirty_write({emqx_user, {get_old_type(), Login}, base64:decode(Password), CreatedAt})
                           end, Auths)
     end.
+
+
 do_import_auth_mnesia(Auths) ->
     case ets:info(emqx_user) of
         undefined -> ok;
@@ -491,7 +506,7 @@ do_import_acl_mnesia_by_old_data(Acls) ->
                                          true -> allow;
                                          false -> deny
                                      end,
-                            mnesia:dirty_write({emqx_acl, {{username, Login}, Topic}, any_to_atom(Action), Allow1, CreatedAt})
+                            mnesia:dirty_write({emqx_acl, {{get_old_type(), Login}, Topic}, any_to_atom(Action), Allow1, CreatedAt})
                           end, Acls)
     end.
 do_import_acl_mnesia(Acls) ->
@@ -599,11 +614,14 @@ do_export_extra_data() ->
 do_export_extra_data() -> [].
 -endif.
 
-import(Filename) ->
+import(Filename, OverridesJson) ->
     case file:read_file(Filename) of
         {ok, Json} ->
-            Data = emqx_json:decode(Json, [return_maps]),
+            Imported = emqx_json:decode(Json, [return_maps]),
+            Overrides = emqx_json:decode(OverridesJson, [return_maps]),
+            Data = maps:merge(Imported, Overrides),
             Version = to_version(maps:get(<<"version">>, Data)),
+            read_global_auth_type(Data, Version),
             case lists:member(Version, ?VERSIONS) of
                 true  ->
                     try
@@ -648,4 +666,28 @@ covert_empty_headers(Headers) ->
         [] -> #{};
         Other -> Other
     end.
--endif.
+-endif.
+
+read_global_auth_type(Data, Version) when Version =:= "4.0" orelse
+                                          Version =:= "4.1" orelse
+                                          Version =:= "4.2" ->
+    case Data of
+        #{<<"auth.mnesia.as">> := <<"username">>} -> application:set_env(emqx_auth_mnesia, as, username);
+        #{<<"auth.mnesia.as">> := <<"clientid">>} -> application:set_env(emqx_auth_mnesia, as, clientid);
+        _ ->
+            logger:error("While importing data from EMQX versions prior to 4.3 "
+                         "it is necessary to specify the value of \"auth.mnesia.as\" parameter "
+                         "as it was configured in etc/plugins/emqx_auth_mnesia.conf.\n"
+                         "Use the following command to import data:\n"
+                         "  $ emqx_ctl data import <filename> --env '{\"auth.mnesia.as\":\"username\"}'\n"
+                         "or\n"
+                         "  $ emqx_ctl data import <filename> --env '{\"auth.mnesia.as\":\"clientid\"}'",
+                         []),
+            error(import_failed)
+    end;
+read_global_auth_type(_Data, _Version) ->
+    ok.
+
+get_old_type() ->
+    {ok, Type} = application:get_env(emqx_auth_mnesia, as),
+    Type.

+ 97 - 0
apps/emqx_management/test/emqx_auth_mnesia_migration_SUITE.erl

@@ -0,0 +1,97 @@
+%%--------------------------------------------------------------------
+%% 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_auth_mnesia_migration_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+
+-include_lib("emqx/include/emqx.hrl").
+-include_lib("emqx/include/emqx_mqtt.hrl").
+-include_lib("emqx_auth_mnesia/include/emqx_auth_mnesia.hrl").
+
+all() ->
+    [{group, Id} || {Id, _, _} <- groups()].
+
+groups() ->
+    [{username, [], cases()}, {clientid, [], cases()}].
+
+cases() ->
+    [t_import_4_2, t_import_4_1].
+
+init_per_suite(Config) ->
+    emqx_ct_helpers:start_apps([emqx_management, emqx_dashboard, emqx_auth_mnesia]),
+    ekka_mnesia:start(),
+    emqx_mgmt_auth:mnesia(boot),
+    Config.
+
+end_per_suite(_Config) ->
+    emqx_ct_helpers:stop_apps([emqx_modules, emqx_management, emqx_dashboard, emqx_management, emqx_auth_mnesia]),
+    ekka_mnesia:ensure_stopped().
+
+init_per_group(username, Config) ->
+    [{cred_type, username} | Config];
+init_per_group(clientid, Config) ->
+    [{cred_type, clientid} | Config].
+
+end_per_group(_, Config) ->
+    Config.
+
+init_per_testcase(_, Config) ->
+    Config.
+
+end_per_testcase(_, _Config) ->
+    mnesia:clear_table(emqx_acl),
+    mnesia:clear_table(emqx_user),
+    ok.
+
+t_import_4_2(Config) ->
+    test_import(Config, "v4.2.json").
+
+t_import_4_1(Config) ->
+    test_import(Config, "v4.1.json").
+
+test_import(Config, File) ->
+    Type = proplists:get_value(cred_type, Config),
+    mnesia:clear_table(emqx_acl),
+    mnesia:clear_table(emqx_user),
+    Filename = filename:join(proplists:get_value(data_dir, Config), File),
+    Overrides = emqx_json:encode(#{<<"auth.mnesia.as">> => atom_to_binary(Type)}),
+    ?assertMatch(ok, emqx_mgmt_data_backup:import(Filename, Overrides)),
+    Records = lists:sort(ets:tab2list(emqx_acl)),
+    %% Check importing of records related to emqx_auth_mnesia
+    ?assertMatch([#emqx_acl{
+                     filter = {{Type,<<"emqx_c">>}, <<"Topic/A">>},
+                     action = pub,
+                     access = allow
+                    },
+                  #emqx_acl{
+                     filter = {{Type,<<"emqx_c">>}, <<"Topic/A">>},
+                     action = sub,
+                     access = allow
+                    }],
+                 lists:sort(Records)),
+    ?assertMatch([#emqx_user{
+                     login = {Type, <<"emqx_c">>}
+                    }], ets:tab2list(emqx_user)),
+    Req = #{clientid => <<"blah">>}
+          #{Type => <<"emqx_c">>,
+            password => "emqx_p"
+           },
+    ?assertMatch({stop, #{auth_result := success}},
+                 emqx_auth_mnesia:check(Req, #{}, #{hash_type => sha256})).

+ 48 - 0
apps/emqx_management/test/emqx_auth_mnesia_migration_SUITE_data/v4.1.json

@@ -0,0 +1,48 @@
+{
+    "acl_mnesia": [
+        {
+            "action": "sub",
+            "allow": true,
+            "login": "emqx_c",
+            "topic": "Topic/A"
+        },
+        {
+            "action": "pub",
+            "allow": true,
+            "login": "emqx_c",
+            "topic": "Topic/A"
+        }
+    ],
+    "apps": [
+        {
+            "desc": "Application user",
+            "expired": "undefined",
+            "id": "admin",
+            "name": "Default",
+            "secret": "public",
+            "status": true
+        }
+    ],
+    "auth_clientid": [],
+    "auth_mnesia": [
+        {
+            "is_superuser": false,
+            "login": "emqx_c",
+            "password": "Y2ViNWU5MTdmNzkzMGFlOGYwZGMzY2ViNDk2YTQyOGY3ZTY0NDczNmVlYmNhMzZhMmI4ZjZiYmFjNzU2MTcxYQ=="
+        }
+    ],
+    "auth_username": [],
+    "blacklist": [],
+    "date": "2021-03-30 09:11:29",
+    "resources": [],
+    "rules": [],
+    "schemas": [],
+    "users": [
+        {
+            "password": "t89PhgOb15rSCdpxm7Obp7QGcyY=",
+            "tags": "administrator",
+            "username": "admin"
+        }
+    ],
+    "version": "4.1"
+}

+ 53 - 0
apps/emqx_management/test/emqx_auth_mnesia_migration_SUITE_data/v4.2.json

@@ -0,0 +1,53 @@
+{
+    "schemas": [],
+    "acl_mnesia": [
+        {
+            "allow": true,
+            "action": "sub",
+            "topic": "Topic/A",
+            "login": "emqx_c"
+        },
+        {
+            "allow": true,
+            "action": "pub",
+            "topic": "Topic/A",
+            "login": "emqx_c"
+        }
+    ],
+    "auth_mnesia": [
+        {
+            "is_superuser": false,
+            "password": "ceb5e917f7930ae8f0dc3ceb496a428f7e644736eebca36a2b8f6bbac756171a",
+            "login": "emqx_c"
+        }
+    ],
+    "auth_username": [],
+    "auth_clientid": [],
+    "users": [
+        {
+            "tags": "viewer",
+            "password": "oVqjR1wOi2u4DtsuXNctYt6+SKE=",
+            "username": "test"
+        },
+        {
+            "tags": "administrator",
+            "password": "9SO4rEEZ6rNwA4vAwp3cnXgQsAM=",
+            "username": "admin"
+        }
+    ],
+    "apps": [
+        {
+            "expired": "undefined",
+            "status": true,
+            "desc": "Application user",
+            "name": "Default",
+            "secret": "public",
+            "id": "admin"
+        }
+    ],
+    "blacklist": [],
+    "resources": [],
+    "rules": [],
+    "date": "2021-03-26 09:51:38",
+    "version": "4.2"
+}