diff --git a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl index 34f9777b9..c4b11dbe9 100644 --- a/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl +++ b/apps/emqx_auth_mnesia/src/emqx_acl_mnesia_cli.erl @@ -122,8 +122,8 @@ cli(_) -> , {"acl list ", "List all acls"} , {"acl show clientid ", "Lookup clientid acl detail"} , {"acl show username ", "Lookup username acl detail"} - , {"acl aad clientid ", "Add clientid acl"} - , {"acl add Username ", "Add username acl"} + , {"acl add clientid ", "Add clientid acl"} + , {"acl add username ", "Add username acl"} , {"acl add _all ", "Add $all acl"} , {"acl delete clientid ", "Delete clientid acl"} , {"acl delete username ", "Delete username acl"} diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src index 6dd1dcdfc..3bce055f6 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.app.src @@ -1,6 +1,6 @@ {application, emqx_auth_mnesia, [{description, "EMQ X Authentication with Mnesia"}, - {vsn, "4.3.9"}, % strict semver, bump manually + {vsn, "4.3.10"}, % strict semver, bump manually {modules, []}, {registered, []}, {applications, [kernel,stdlib,mnesia]}, diff --git a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src index 7906449db..5cf05d34d 100644 --- a/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src +++ b/apps/emqx_auth_mnesia/src/emqx_auth_mnesia.appup.src @@ -1,7 +1,9 @@ %% -*- mode: erlang -*- %% Unless you know what you are doing, DO NOT edit manually!! {VSN, - [{"4.3.7", + [{<<"4\\.3\\.[8-9]">>, + [{load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, + {"4.3.7", [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, {<<"4\\.3\\.[5-6]">>, @@ -33,7 +35,9 @@ {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mnesia_app,brutal_purge,soft_purge,[]}]}, {<<".*">>,[]}], - [{"4.3.7", + [{<<"4\\.3\\.[8-9]">>, + [{load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, + {"4.3.7", [{load_module,emqx_auth_mnesia_api,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, {<<"4\\.3\\.[5-6]">>, diff --git a/changes/v4.3.22-en.md b/changes/v4.3.22-en.md index bac959ca9..6b5ed74b7 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 administrative username and password when EMQX initializes the database [#9256](https://github.com/emqx/emqx/pull/9256). + + ## 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..8fb86bc22 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 初始化数据库时,从该文件批量导入一些控制台用户的用户名 / 密码 [#9256](https://github.com/emqx/emqx/pull/9256)。 + + ## 修复 - 修复若上传的备份文件名中包含 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..18756f06a 100644 --- a/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf +++ b/lib-ce/emqx_dashboard/etc/emqx_dashboard.conf @@ -17,6 +17,16 @@ dashboard.default_user.login = admin ## Value: String dashboard.default_user.password = public +## Initialize users file +## Is used to add administrative dashboard users when EMQX is launched for the first time. +## This config will not take any effect once EMQX database is populated with the provided users. +## The file content format is as below: +## ``` +##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_admin.erl b/lib-ce/emqx_dashboard/src/emqx_dashboard_admin.erl index a76ed9cff..223d99fa5 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, <<"bootstrapped">>). -boot_mnesia({mnesia, [boot]}). -copy_mnesia({mnesia, [copy]}). @@ -43,6 +44,7 @@ , change_password/3 , all_users/0 , check/2 + , init_bootstrap_users/0 ]). %% gen_server Function Exports @@ -195,6 +197,67 @@ check(Username, Password) -> {error, <<"Username/Password error">>} end. +init_bootstrap_users() -> + Bootstrap = application:get_env(emqx_dashboard, bootstrap_users_file, undefined), + Size = mnesia:table_info(mqtt_admin, size), + init_bootstrap_users(Bootstrap, Size). + +init_bootstrap_users(undefined, _) -> ok; +init_bootstrap_users(_File, Size)when Size > 0 -> ok; +init_bootstrap_users(File, 0) -> + case file:open(File, [read, binary]) of + {ok, Dev} -> + {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]), + case init_bootstrap_users(File, Dev, MP) of + ok -> ok; + Error -> + %% if failed add bootstrap users, we should clear all bootstrap users + {atomic, ok} = mnesia:clear_table(mqtt_admin), + Error + end; + {error, Reason} = Error -> + ?LOG(error, + "failed to open the dashboard bootstrap users file(~s) for ~p", + [File, Reason] + ), + Error + end. + +init_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); + {error, 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, Error} -> + throw(#{file => File, line => Line, reason => Error}) + end. + bad_login_penalty() -> timer:sleep(2000), ok. @@ -207,6 +270,12 @@ is_valid_pwd(<>, Password) -> %%-------------------------------------------------------------------- init([]) -> + case init_bootstrap_users() of + ok -> init_default_admin_user(); + {error, Error} -> {stop, Error} + end. + +init_default_admin_user() -> case binenv(default_user_username) of <<>> -> ok; UserName -> diff --git a/lib-ce/emqx_dashboard/test/emqx_dashboard_admin_bootstrap_user.erl b/lib-ce/emqx_dashboard/test/emqx_dashboard_admin_bootstrap_user.erl new file mode 100644 index 000000000..452cfc25a --- /dev/null +++ b/lib-ce/emqx_dashboard/test/emqx_dashboard_admin_bootstrap_user.erl @@ -0,0 +1,89 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 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_dashboard_admin_bootstrap_user). + +-compile(export_all). +-compile(nowarn_export_all). +-import(emqx_dashboard_SUITE, [http_post/2]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx/include/emqx.hrl"). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + Config. + +end_per_suite(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_load_ok(_) -> + Bin = <<"test-1:password-1\ntest-2:password-2">>, + File = "./bootstrap_users.txt", + ok = file:write_file(File, Bin), + _ = mnesia:clear_table(emqx_admin), + application:set_env(emqx_dashboard, bootstrap_users_file, File), + emqx_ct_helpers:start_apps([emqx_dashboard]), + ?assertEqual(#{<<"code">> => 0}, check_auth(<<"test-1">>, <<"password-1">>)), + ?assertEqual(#{<<"code">> => 0}, check_auth(<<"test-2">>, <<"password-2">>)), + ?assertEqual(#{<<"message">> => <<"Username/Password error">>}, + check_auth(<<"test-2">>, <<"password-1">>)), + emqx_ct_helpers:stop_apps([emqx_dashboard]). + +t_bootstrap_user_file_not_found(_) -> + File = "./bootstrap_users_not_exist.txt", + check_load_failed(File), + ok. + +t_load_invalid_username_failed(_) -> + Bin = <<"test-1:password-1\ntest&2:password-2">>, + File = "./bootstrap_users.txt", + ok = file:write_file(File, Bin), + check_load_failed(File), + ok. + +t_load_invalid_format_failed(_) -> + Bin = <<"test-1:password-1\ntest-2password-2">>, + File = "./bootstrap_users.txt", + ok = file:write_file(File, Bin), + check_load_failed(File), + ok. + +check_load_failed(File) -> + _ = mnesia:clear_table(emqx_admin), + application:set_env(emqx_dashboard, bootstrap_users_file, File), + ?assertError(_, emqx_ct_helpers:start_apps([emqx_dashboard])), + ?assertNot(lists:member(emqx_dashboard, application:which_applications())), + ?assertEqual(0, mnesia:table_info(mqtt_admin, size)). + + +check_auth(Username, Password) -> + {ok, Res} = http_post("auth", #{<<"username">> => Username, <<"password">> => Password}), + json(Res). + +json(Data) -> + {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.