Merge pull request #9256 from zhongwencool/bootstrap-users
feat: bootstrap dashboard users from dashboard.bootstrap_users_file
This commit is contained in:
commit
84e089260f
|
@ -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"}
|
||||||
|
|
|
@ -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]},
|
||||||
|
|
|
@ -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]">>,
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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)。
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue