Merge pull request #9256 from zhongwencool/bootstrap-users

feat: bootstrap dashboard users from dashboard.bootstrap_users_file
This commit is contained in:
zhongwencool 2022-10-31 09:42:56 +08:00 committed by GitHub
commit 84e089260f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 5 deletions

View File

@ -122,8 +122,8 @@ cli(_) ->
, {"acl list ", "List all acls"} , {"acl list ", "List all acls"}
, {"acl show clientid <Clientid>", "Lookup clientid acl detail"} , {"acl show clientid <Clientid>", "Lookup clientid acl detail"}
, {"acl show username <Username>", "Lookup username acl detail"} , {"acl show username <Username>", "Lookup username acl detail"}
, {"acl aad clientid <Clientid> <Topic> <Action> <Access>", "Add clientid acl"} , {"acl add clientid <Clientid> <Topic> <Action> <Access>", "Add clientid acl"}
, {"acl add Username <Username> <Topic> <Action> <Access>", "Add username acl"} , {"acl add username <Username> <Topic> <Action> <Access>", "Add username acl"}
, {"acl add _all <Topic> <Action> <Access>", "Add $all acl"} , {"acl add _all <Topic> <Action> <Access>", "Add $all acl"}
, {"acl delete clientid <Clientid> <Topic>", "Delete clientid acl"} , {"acl delete clientid <Clientid> <Topic>", "Delete clientid acl"}
, {"acl delete username <Username> <Topic>", "Delete username acl"} , {"acl delete username <Username> <Topic>", "Delete username acl"}

View File

@ -1,6 +1,6 @@
{application, emqx_auth_mnesia, {application, emqx_auth_mnesia,
[{description, "EMQ X Authentication with Mnesia"}, [{description, "EMQ X Authentication with Mnesia"},
{vsn, "4.3.9"}, % strict semver, bump manually {vsn, "4.3.10"}, % strict semver, bump manually
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [kernel,stdlib,mnesia]}, {applications, [kernel,stdlib,mnesia]},

View File

@ -1,7 +1,9 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
%% Unless you know what you are doing, DO NOT edit manually!! %% Unless you know what you are doing, DO NOT edit manually!!
{VSN, {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_auth_mnesia_api,brutal_purge,soft_purge,[]},
{load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]},
{<<"4\\.3\\.[5-6]">>, {<<"4\\.3\\.[5-6]">>,
@ -33,7 +35,9 @@
{load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}, {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mnesia_app,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_auth_mnesia_api,brutal_purge,soft_purge,[]},
{load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]}, {load_module,emqx_acl_mnesia_cli,brutal_purge,soft_purge,[]}]},
{<<"4\\.3\\.[5-6]">>, {<<"4\\.3\\.[5-6]">>,

View File

@ -17,6 +17,9 @@
- Enhanced log security in ACL modules, sensitive data will be obscured. [#9242](https://github.com/emqx/emqx/pull/9242). - 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 ## 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). - 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).

View File

@ -17,6 +17,9 @@
- 增强 ACL 模块中的日志安全性,敏感数据将被模糊化。[#9242](https://github.com/emqx/emqx/pull/9242)。 - 增强 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)。 - 修复若上传的备份文件名中包含 UTF8 字符,`GET /data/export` HTTP 接口返回 500 错误 [#9224](https://github.com/emqx/emqx/pull/9224)。

View File

@ -17,6 +17,16 @@ dashboard.default_user.login = admin
## Value: String ## Value: String
dashboard.default_user.password = public 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 ## HTTP Listener

View File

@ -10,6 +10,11 @@
{override_env, "ADMIN_PASSWORD"} {override_env, "ADMIN_PASSWORD"}
]}. ]}.
{mapping, "dashboard.bootstrap_users_file", "emqx_dashboard.bootstrap_users_file", [
{datatype, string},
hidden
]}.
{mapping, "dashboard.listener.http", "emqx_dashboard.listeners", [ {mapping, "dashboard.listener.http", "emqx_dashboard.listeners", [
{datatype, [integer, ip]} {datatype, [integer, ip]}
]}. ]}.

View File

@ -23,6 +23,7 @@
-include("emqx_dashboard.hrl"). -include("emqx_dashboard.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-define(DEFAULT_PASSWORD, <<"public">>). -define(DEFAULT_PASSWORD, <<"public">>).
-define(BOOTSTRAP_USER_TAG, <<"bootstrapped">>).
-boot_mnesia({mnesia, [boot]}). -boot_mnesia({mnesia, [boot]}).
-copy_mnesia({mnesia, [copy]}). -copy_mnesia({mnesia, [copy]}).
@ -43,6 +44,7 @@
, change_password/3 , change_password/3
, all_users/0 , all_users/0
, check/2 , check/2
, init_bootstrap_users/0
]). ]).
%% gen_server Function Exports %% gen_server Function Exports
@ -195,6 +197,67 @@ check(Username, Password) ->
{error, <<"Username/Password error">>} {error, <<"Username/Password error">>}
end. 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() -> bad_login_penalty() ->
timer:sleep(2000), timer:sleep(2000),
ok. ok.
@ -207,6 +270,12 @@ is_valid_pwd(<<Salt:4/binary, Hash/binary>>, Password) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
init([]) -> 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 case binenv(default_user_username) of
<<>> -> ok; <<>> -> ok;
UserName -> UserName ->

View File

@ -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.