diff --git a/changes/v4.3.22-en.md b/changes/v4.3.22-en.md index bac959ca9..7bb224925 100644 --- a/changes/v4.3.22-en.md +++ b/changes/v4.3.22-en.md @@ -17,6 +17,9 @@ - Enhanced log security in ACL modules, sensitive data will be obscured. [#9242](https://github.com/emqx/emqx/pull/9242). +- Add `dashboard.bootstrap_users_file` configuration to bulk import default user&password when EMQX first starts. + + ## Bug fixes - Fix that after uploading a backup file with an UTF8 filename, HTTP API `GET /data/export` fails with status code 500 [#9224](https://github.com/emqx/emqx/pull/9224). diff --git a/changes/v4.3.22-zh.md b/changes/v4.3.22-zh.md index 286b2a2f0..bc5ea5607 100644 --- a/changes/v4.3.22-zh.md +++ b/changes/v4.3.22-zh.md @@ -17,6 +17,9 @@ - 增强 ACL 模块中的日志安全性,敏感数据将被模糊化。[#9242](https://github.com/emqx/emqx/pull/9242)。 +- 增加 `dashboard.bootstrap_users_file` 配置,可以在EMQX第一次启动时批量导入默认的用户/密码。 + + ## 修复 - 修复若上传的备份文件名中包含 UTF8 字符,`GET /data/export` HTTP 接口返回 500 错误 [#9224](https://github.com/emqx/emqx/pull/9224)。 diff --git a/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf b/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf index f59f27a47..7de3dbbf4 100644 --- a/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf +++ b/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf @@ -17,6 +17,15 @@ dashboard.default_user.login = admin ## Value: String dashboard.default_user.password = public +## Initialize users file +## Is used to add an administrative user to Dashboard when emqx is first launched, +## the format is: +## ``` +##username1:password1 +##username2:password2 +## ``` +dashboard.bootstrap_users_file = {{ platform_etc_dir }}/bootstrap_users.txt + ##-------------------------------------------------------------------- ## HTTP Listener diff --git a/lib-ce/emqx_dashboard/priv/emqx_dashboard.schema b/lib-ce/emqx_dashboard/priv/emqx_dashboard.schema index 7ef39ac8d..93607b61b 100644 --- a/lib-ce/emqx_dashboard/priv/emqx_dashboard.schema +++ b/lib-ce/emqx_dashboard/priv/emqx_dashboard.schema @@ -10,6 +10,11 @@ {override_env, "ADMIN_PASSWORD"} ]}. +{mapping, "dashboard.bootstrap_users_file", "emqx_dashboard.bootstrap_users_file", [ + {datatype, string}, + hidden +]}. + {mapping, "dashboard.listener.http", "emqx_dashboard.listeners", [ {datatype, [integer, ip]} ]}. diff --git a/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src b/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src index 07c67545b..cb36b99a8 100644 --- a/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src +++ b/lib-ce/emqx_dashboard/src/emqx_dashboard.app.src @@ -1,6 +1,6 @@ {application, emqx_dashboard, [{description, "EMQ X Web Dashboard"}, - {vsn, "4.3.18"}, % strict semver, bump manually! + {vsn, "4.3.19"}, % strict semver, bump manually! {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel,stdlib,mnesia,minirest]}, diff --git a/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl b/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl index a76ed9cff..5ebe18221 100644 --- a/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -23,6 +23,7 @@ -include("emqx_dashboard.hrl"). -include_lib("emqx/include/logger.hrl"). -define(DEFAULT_PASSWORD, <<"public">>). +-define(BOOTSTRAP_USER_TAG, <<"bootstrap user">>). -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -43,6 +44,7 @@ , change_password/3 , all_users/0 , check/2 + , add_bootstrap_users/0 ]). %% gen_server Function Exports @@ -195,6 +197,75 @@ check(Username, Password) -> {error, <<"Username/Password error">>} end. +add_bootstrap_users() -> + Bootstrap = application:get_env(emqx_dashboard, bootstrap_users_file, undefined), + Size = mnesia:table_info(mqtt_admin, size), + add_bootstrap_users(Bootstrap, Size). + +add_bootstrap_users(undefined, _) -> ok; +add_bootstrap_users(_File, Size)when Size > 0 -> ok; +add_bootstrap_users(File, 0) -> + case file:open(File, [read, binary]) of + {ok, Dev} -> + {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]), + case add_bootstrap_users(File, Dev, MP) of + ok -> ok; + Error -> + %% if failed add bootstrap users, we should clear all bootstrap users + mnesia:transaction(fun clear_bootstrap_users/0, []), + Error + end; + {error, Reason} = Error -> + ?LOG(error, + "failed to open the dashboard bootstrap users file(~s) for ~p", + [File, Reason] + ), + Error + end. + +add_bootstrap_users(File, Dev, MP) -> + try + add_bootstrap_user(File, Dev, MP, 1) + catch + throw:Error -> {error, Error}; + Type:Reason:Stacktrace -> + {error, {Type, Reason, Stacktrace}} + after + file:close(Dev) + end. + +add_bootstrap_user(File, Dev, MP, Line) -> + case file:read_line(Dev) of + {ok, Bin} -> + case re:run(Bin, MP, [global, {capture, all_but_first, binary}]) of + {match, [[Username, Password]]} -> + case add_user(Username, Password, ?BOOTSTRAP_USER_TAG) of + ok -> + add_bootstrap_user(File, Dev, MP, Line + 1); + Reason -> + throw(#{file => File, line => Line, content => Bin, reason => Reason}) + end; + _ -> + ?LOG(error, + "failed to bootstrap users file(~s) for Line(~w): ~ts", + [File, Line, Bin] + ), + throw(#{file => File, line => Line, content => Bin, reason => "invalid format"}) + end; + eof -> + ok; + Error -> + throw(#{file => File, line => Line, reason => Error}) + end. + +clear_bootstrap_users() -> + FoldFun = + fun(#mqtt_admin{tags = ?BOOTSTRAP_USER_TAG} = User, Acc) -> + mnesia:delete_object(User), Acc; + (_, Acc) -> Acc + end, + mnesia:foldl(FoldFun, ok, mqtt_admin). + bad_login_penalty() -> timer:sleep(2000), ok. @@ -207,16 +278,20 @@ is_valid_pwd(<>, Password) -> %%-------------------------------------------------------------------- init([]) -> - case binenv(default_user_username) of - <<>> -> ok; - UserName -> - %% Add default admin user - {ok, _} = mnesia:subscribe({table, mqtt_admin, simple}), - PasswordHash = ensure_default_user_in_db(UserName), - ok = ensure_default_user_passwd_hashed_in_pt(PasswordHash), - ok = maybe_warn_default_pwd() - end, - {ok, state}. + case add_bootstrap_users() of + ok -> + case binenv(default_user_username) of + <<>> -> ok; + UserName -> + %% Add default admin user + {ok, _} = mnesia:subscribe({table, mqtt_admin, simple}), + PasswordHash = ensure_default_user_in_db(UserName), + ok = ensure_default_user_passwd_hashed_in_pt(PasswordHash), + ok = maybe_warn_default_pwd() + end, + {ok, state}; + Error -> {stop, Error} + end. handle_call(_Req, _From, State) -> {reply, error, State}.