diff --git a/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl b/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl index c9e9bee3c..b4e17f05b 100644 --- a/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -21,7 +21,6 @@ -behaviour(gen_server). -include("emqx_dashboard.hrl"). --include_lib("emqx/include/logger.hrl"). -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -140,9 +139,9 @@ update_pwd(Username, Fun) -> Trans = fun() -> User = case lookup_user(Username) of - [Admin] -> Admin; - [] -> - mnesia:abort(<<"Username Not Found">>) + [Admin] -> Admin; + [] -> + mnesia:abort(<<"Username Not Found">>) end, mnesia:write(Fun(User)) end, @@ -150,7 +149,13 @@ update_pwd(Username, Fun) -> -spec(lookup_user(binary()) -> [mqtt_admin()]). -lookup_user(Username) when is_binary(Username) -> mnesia:dirty_read(mqtt_admin, Username). +lookup_user(Username) when is_binary(Username) -> + case binenv(default_user_username) of + Username -> + [#mqtt_admin{username=Username, password=hashed_default_passwd()}]; + _ -> + mnesia:dirty_read(mqtt_admin, Username) + end. -spec(all_users() -> [#mqtt_admin{}]). all_users() -> ets:tab2list(mqtt_admin). @@ -181,7 +186,7 @@ check(Username, Password) -> init([]) -> %% Add default admin user - _ = add_default_user(binenv(default_user_username), binenv(default_user_passwd)), + {ok, _} = mnesia:subscribe({table, mqtt_admin, simple}), {ok, state}. handle_call(_Req, _From, State) -> @@ -190,6 +195,17 @@ handle_call(_Req, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. +handle_info({mnesia_table_event, {write, Admin, _}}, State) -> + #mqtt_admin{username=Username, password=HashedPassword} = Admin, + case binenv(default_user_username) of + Username -> + application:set_env(emqx_dashboard, default_user_passwd_hashed, HashedPassword); + + _ -> + ignore + end, + {noreply, State}; + handle_info(_Msg, State) -> {noreply, State}. @@ -216,37 +232,15 @@ salt() -> <>. binenv(Key) -> - iolist_to_binary(application:get_env(emqx_dashboard, Key, "")). + iolist_to_binary(application:get_env(emqx_dashboard, Key, <<>>)). -add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY(Password) -> - ignore; - -add_default_user(Username, Password) -> - case lookup_user(Username) of - [] -> add_user(Username, Password, <<"administrator">>); - _ -> - case check(Username, Password) of - ok -> - ?LOG(warning, - "[Dashboard] The initial default password for dashboard 'admin' user in emqx_dashboard.conf\n" - "For safety, it should be changed as soon as possible.\n" - "Please use the './bin/emqx_ctl admins' CLI to change it.\n" - "Then remove `dashboard.default_user.login/password` from emqx_dashboard.conf" - ); - {error, _} -> - %% We can't force add default, - %% otherwise passwords that have been updated via HTTP API will be reset after reboot. - ?LOG(warning, - "[Dashboard] dashboard.default_user.password in the plugins/emqx_dashboard.conf\n" - "does not match the password in the database(mnesia).\n" - "1. If you have already changed the password via the HTTP API or `./bin/emqx_ctl admins`," - "this warning has no effect.\n" - "You should remove the `dashboard.default_user.login/password` from emqx_dashboard.conf " - "to resolve this warning.\n" - "2. If you just want to update the password by manually changing the configuration file,\n" - "you need to delete the old user and password using `emqx_ctl admins del ~s` first\n" - "the new password in emqx_dashboard.conf can take effect after reboot.", - []) - end - end, - ok. +hashed_default_passwd() -> + case binenv(default_user_passwd_hashed) of + Empty0 when ?EMPTY_KEY(Empty0) -> + case binenv(default_user_passwd) of + Empty when ?EMPTY_KEY(Empty) -> + undefined; + Password -> hash(Password) + end; + HashedPassword -> HashedPassword + end. diff --git a/lib-ce/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/lib-ce/emqx_dashboard/test/emqx_dashboard_SUITE.erl index ef2e747fa..ddfcb540a 100644 --- a/lib-ce/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/lib-ce/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -29,6 +29,8 @@ -include_lib("emqx/include/emqx.hrl"). +-include("emqx_dashboard.hrl"). + -define(CONTENT_TYPE, "application/x-www-form-urlencoded"). -define(HOST, "http://127.0.0.1:18083/"). @@ -40,21 +42,23 @@ -define(OVERVIEWS, ['alarms/activated', 'alarms/deactivated', banned, brokers, stats, metrics, listeners, clients, subscriptions, routes, plugins]). all() -> - [{group, overview}, + [ + {group, overview}, {group, admins}, {group, rest}, {group, cli} ]. groups() -> - [{overview, [sequence], [t_overview]}, - {admins, [sequence], [t_admins_add_delete]}, + [ + {overview, [sequence], [t_overview]}, + {admins, [sequence], [t_default_password_persists_after_leaving_cluster]}, {rest, [sequence], [t_rest_api, t_auth_exhaustive_attack]}, {cli, [sequence], [t_cli]} ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_dashboard]), + ok = emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_dashboard]), Config. end_per_suite(_Config) -> @@ -79,6 +83,77 @@ t_admins_add_delete(_) -> ok = emqx_dashboard_admin:remove_user(<<"username">>), ?assertNotEqual(true, request_dashboard(get, api_path("brokers"), auth_header_("username", "pwd"))). +t_admins_persist_default_password(_) -> + emqx_dashboard_admin:change_password(<<"admin">>, <<"new_password">>), + [#mqtt_admin{password=Password}] = emqx_dashboard_admin:lookup_user(<<"admin">>), + + %% To ensure that state persists even if the process dies + exit(whereis(emqx_dashboard_admin), kill), + + %% It get's restarted by the app automatically + [#mqtt_admin{password=PasswordAfterRestart}] = emqx_dashboard_admin:lookup_user(<<"admin">>), + ?assertEqual(Password, PasswordAfterRestart), + emqx_dashboard_admin:change_password(<<"admin">>, <<"public">>). + +debug(Label, Slave) -> + ct:print( + "[~p]~nusers local ~p~nusers remote: ~p~nenv local: ~p~nenv remote: ~p", + [ + Label, + ets:tab2list(mqtt_admin), + rpc:call(Slave, ets, tab2list, [mqtt_admin]), + application:get_all_env(emqx_dashboard), + rpc:call(Slave, application, get_all_env, [emqx_dashboard]) + ]). + + +t_default_password_persists_after_leaving_cluster(_) -> + [#mqtt_admin{password=InitialPassword}] = emqx_dashboard_admin:lookup_user(<<"admin">>), + + ct:print("Cluster status: ~p", [ekka_cluster:info()]), + ct:print("Table nodes: ~p", [mnesia:table_info(mqtt_admin, active_replicas)]), + + Slave = start_slave('test1', [emqx_modules, emqx_management, emqx_dashboard]), + + %% To make sure that subscription is not lost during reconnection + rpc:call(Slave, ekka, leave, []), + ct:sleep(100), %% To ensure that leave gets processed + rpc:call(Slave, ekka, join, [node()]), + ct:sleep(100), %% To ensure that join gets processed + + ct:print("Cluster status: ~p", [ekka_cluster:info()]), + ct:print("Table nodes: ~p", [mnesia:table_info(mqtt_admin, active_replicas)]), + + ct:print("Apps: ~p", [ + rpc:call(Slave, application, which_applications, []) + ]), + + debug(0, Slave), + + emqx_dashboard_admin:change_password(<<"admin">>, <<"new_password">>), + ct:sleep(100), %% To ensure that event gets processed + + debug(1, Slave), + + [#mqtt_admin{password=Password}] = rpc:call(Slave, emqx_dashboard_admin, lookup_user, [<<"admin">>]), + ?assertNotEqual(InitialPassword, Password), + + rpc:call(Slave, ekka, leave, []), + + debug(2, Slave), + + ?assertEqual( + ok, + rpc:call(Slave, emqx_dashboard_admin, check, [<<"admin">>, <<"new_password">>])), + + ?assertMatch( + {error, _}, + rpc:call(Slave, emqx_dashboard_admin, check, [<<"admin">>, <<"password">>])), + + {ok, _} = stop_slave(Slave, [emqx_dashboard, emqx_management, emqx_modules]), + + emqx_dashboard_admin:change_password(<<"admin">>, <<"public">>). + t_rest_api(_Config) -> {ok, Res0} = http_get("users"), @@ -166,3 +241,54 @@ api_path(Path) -> json(Data) -> {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx. +start_slave(Name, Apps) -> + {ok, Node} = ct_slave:start(list_to_atom(atom_to_list(Name) ++ "@" ++ host()), + [{kill_if_fail, true}, + {monitor_master, true}, + {init_timeout, 10000}, + {startup_timeout, 10000}, + {erl_flags, ebin_path()}]), + + pong = net_adm:ping(Node), + setup_node(Node, Apps), + Node. + +stop_slave(Node, Apps) -> + [ok = Res || Res <- rpc:call(Node, emqx_ct_helpers, stop_apps, [Apps])], + rpc:call(Node, ekka, leave, []), + ct_slave:stop(Node). + +host() -> + [_, Host] = string:tokens(atom_to_list(node()), "@"), Host. + +ebin_path() -> + string:join(["-pa" | lists:filter(fun is_lib/1, code:get_path())], " "). + +is_lib(Path) -> + string:prefix(Path, code:lib_dir()) =:= nomatch. + +setup_node(Node, Apps) -> + EnvHandler = + fun(emqx) -> + application:set_env(emqx, listeners, []), + application:set_env(gen_rpc, port_discovery, manual), + mnesia:info(), + ok; + (emqx_management) -> + application:set_env(emqx_management, listeners, []), + ok; + (emqx_dashboard) -> + application:set_env(emqx_dashboard, listeners, []), + ok; + (_) -> + ok + end, + + [ok = rpc:call(Node, application, load, [App]) || App <- [gen_rpc, emqx | Apps]], + ok = rpc:call(Node, emqx_ct_helpers, start_apps, [Apps, EnvHandler]), + + rpc:call(Node, ekka, join, [node()]), + rpc:call(Node, application, stop, [emqx_dashboard]), + rpc:call(Node, application, start, [emqx_dashboard]), + + ok. diff --git a/src/emqx.erl b/src/emqx.erl index 5c90cf953..ff369e599 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -234,7 +234,15 @@ shutdown(Reason) -> ). reboot() -> - lists:foreach(fun application:start/1 , default_started_applications()). + case application_controller:is_running(emqx_dashboard) of + true -> + application:stop(emqx_dashboard), %% dashboard must be started after mnesia + lists:foreach(fun application:start/1 , default_started_applications()), + application:start(emqx_dashboard); + + false -> + lists:foreach(fun application:start/1 , default_started_applications()) + end. -ifdef(EMQX_ENTERPRISE). default_started_applications() ->