Merge pull request #9774 from zhongwencool/dashboard-password-validate-v50

feat: add dashboard password validation
This commit is contained in:
zhongwencool 2023-01-17 17:02:51 +08:00 committed by GitHub
commit cb5314bb9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 155 additions and 41 deletions

View File

@ -175,6 +175,7 @@ start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) ->
%% Because, minirest, ekka etc.. application will scan these modules %% Because, minirest, ekka etc.. application will scan these modules
lists:foreach(fun load/1, [emqx | Apps]), lists:foreach(fun load/1, [emqx | Apps]),
ok = start_ekka(), ok = start_ekka(),
mnesia:clear_table(emqx_admin),
ok = emqx_ratelimiter_SUITE:load_conf(), ok = emqx_ratelimiter_SUITE:load_conf(),
lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]). lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]).

View File

@ -92,21 +92,79 @@ add_default_user() ->
add_user(Username, Password, Desc) when add_user(Username, Password, Desc) when
is_binary(Username), is_binary(Password) is_binary(Username), is_binary(Password)
-> ->
case legal_username(Username) of case {legal_username(Username), legal_password(Password)} of
true -> {ok, ok} -> do_add_user(Username, Password, Desc);
return( {{error, Reason}, _} -> {error, Reason};
mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]) {_, {error, Reason}} -> {error, Reason}
); end.
false ->
do_add_user(Username, Password, Desc) ->
Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]),
return(Res).
%% 0-9 or A-Z or a-z or $_
legal_username(<<>>) ->
{error, <<"Username cannot be empty">>};
legal_username(UserName) ->
case re:run(UserName, "^[_a-zA-Z0-9]*$", [{capture, none}]) of
nomatch ->
{error, << {error, <<
"Bad Username." "Bad Username."
" Only upper and lower case letters, numbers and underscores are supported" " Only upper and lower case letters, numbers and underscores are supported"
>>} >>};
match ->
ok
end. end.
%% 0 - 9 or A -Z or a - z or $_ -define(LOW_LETTER_CHARS, "abcdefghijklmnopqrstuvwxyz").
legal_username(<<>>) -> false; -define(UPPER_LETTER_CHARS, "ABCDEFGHIJKLMNOPQRSTUVWXYZ").
legal_username(UserName) -> nomatch /= re:run(UserName, "^[_a-zA-Z0-9]*$"). -define(LETTER, ?LOW_LETTER_CHARS ++ ?UPPER_LETTER_CHARS).
-define(NUMBER, "0123456789").
-define(SPECIAL_CHARS, "!@#$%^&*()_+-=[]{}\"|;':,./<>?`~ ").
-define(INVALID_PASSWORD_MSG, <<
"Bad password. "
"At least two different kind of characters from groups of letters, numbers, and special characters. "
"For example, if password is composed from letters, it must contain at least one number or a special character."
>>).
-define(BAD_PASSWORD_LEN, <<"The range of password length is 8~64">>).
legal_password(Password) when is_binary(Password) ->
legal_password(binary_to_list(Password));
legal_password(Password) when is_list(Password) ->
legal_password(Password, erlang:length(Password)).
legal_password(Password, Len) when Len >= 8 andalso Len =< 64 ->
case is_mixed_password(Password) of
true -> ascii_character_validate(Password);
false -> {error, ?INVALID_PASSWORD_MSG}
end;
legal_password(_Password, _Len) ->
{error, ?BAD_PASSWORD_LEN}.
%% The password must contain at least two different kind of characters
%% from groups of letters, numbers, and special characters.
is_mixed_password(Password) -> is_mixed_password(Password, [?NUMBER, ?LETTER, ?SPECIAL_CHARS], 0).
is_mixed_password(_Password, _Chars, 2) ->
true;
is_mixed_password(_Password, [], _Count) ->
false;
is_mixed_password(Password, [Chars | Rest], Count) ->
NewCount =
case contain(Password, Chars) of
true -> Count + 1;
false -> Count
end,
is_mixed_password(Password, Rest, NewCount).
%% regex-non-ascii-character, such as Chinese, Japanese, Korean, etc.
ascii_character_validate(Password) ->
case re:run(Password, "[^\\x00-\\x7F]+", [unicode, {capture, none}]) of
match -> {error, <<"Only ascii characters are allowed in the password">>};
nomatch -> ok
end.
contain(Xs, Spec) -> lists:any(fun(X) -> lists:member(X, Spec) end, Xs).
%% black-magic: force overwrite a user %% black-magic: force overwrite a user
force_add_user(Username, Password, Desc) -> force_add_user(Username, Password, Desc) ->
@ -188,7 +246,10 @@ change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
end. end.
change_password(Username, Password) when is_binary(Username), is_binary(Password) -> change_password(Username, Password) when is_binary(Username), is_binary(Password) ->
change_password_hash(Username, hash(Password)). case legal_password(Password) of
ok -> change_password_hash(Username, hash(Password));
Error -> Error
end.
change_password_hash(Username, PasswordHash) -> change_password_hash(Username, PasswordHash) ->
ChangePWD = ChangePWD =
@ -292,6 +353,45 @@ add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY
{ok, empty}; {ok, empty};
add_default_user(Username, Password) -> add_default_user(Username, Password) ->
case lookup_user(Username) of case lookup_user(Username) of
[] -> add_user(Username, Password, <<"administrator">>); [] -> do_add_user(Username, Password, <<"administrator">>);
_ -> {ok, default_user_exists} _ -> {ok, default_user_exists}
end. end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
legal_password_test() ->
?assertEqual({error, ?BAD_PASSWORD_LEN}, legal_password(<<"123">>)),
MaxPassword = iolist_to_binary([lists:duplicate(63, "x"), "1"]),
?assertEqual(ok, legal_password(MaxPassword)),
TooLongPassword = lists:duplicate(65, "y"),
?assertEqual({error, ?BAD_PASSWORD_LEN}, legal_password(TooLongPassword)),
?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(<<"12345678">>)),
?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?LETTER)),
?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?NUMBER)),
?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?SPECIAL_CHARS)),
?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(<<"映映映映无天在请"/utf8>>)),
?assertEqual(
{error, <<"Only ascii characters are allowed in the password">>},
legal_password(<<"test_for_non_ascii1中"/utf8>>)
),
?assertEqual(
{error, <<"Only ascii characters are allowed in the password">>},
legal_password(<<"云☁test_for_unicode"/utf8>>)
),
?assertEqual(ok, legal_password(?LOW_LETTER_CHARS ++ ?NUMBER)),
?assertEqual(ok, legal_password(?UPPER_LETTER_CHARS ++ ?NUMBER)),
?assertEqual(ok, legal_password(?LOW_LETTER_CHARS ++ ?SPECIAL_CHARS)),
?assertEqual(ok, legal_password(?UPPER_LETTER_CHARS ++ ?SPECIAL_CHARS)),
?assertEqual(ok, legal_password(?SPECIAL_CHARS ++ ?NUMBER)),
?assertEqual(ok, legal_password(<<"abckldiekflkdf12">>)),
?assertEqual(ok, legal_password(<<"abckldiekflkdf w">>)),
?assertEqual(ok, legal_password(<<"# abckldiekflkdf w">>)),
?assertEqual(ok, legal_password(<<"# 12344858">>)),
?assertEqual(ok, legal_password(<<"# %12344858">>)),
ok.
-endif.

View File

@ -61,6 +61,7 @@ end_suite() ->
end_suite(Apps) -> end_suite(Apps) ->
application:unload(emqx_management), application:unload(emqx_management),
mnesia:clear_table(?ADMIN),
emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]).
init_per_suite(Config) -> init_per_suite(Config) ->
@ -82,16 +83,17 @@ set_special_configs(_) ->
t_overview(_) -> t_overview(_) ->
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"simple_description">>), emqx_dashboard_admin:add_user(<<"admin">>, <<"public_www1">>, <<"simple_description">>),
Headers = auth_header_(<<"admin">>, <<"public_www1">>),
[ [
{ok, _} = request_dashboard(get, api_path([Overview]), auth_header_()) {ok, _} = request_dashboard(get, api_path([Overview]), Headers)
|| Overview <- ?OVERVIEWS || Overview <- ?OVERVIEWS
]. ].
t_admins_add_delete(_) -> t_admins_add_delete(_) ->
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
Desc = <<"simple description">>, Desc = <<"simple description">>,
{ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password">>, Desc), {ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password_0">>, Desc),
{ok, _} = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, Desc), {ok, _} = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, Desc),
Admins = emqx_dashboard_admin:all_users(), Admins = emqx_dashboard_admin:all_users(),
?assertEqual(2, length(Admins)), ?assertEqual(2, length(Admins)),
@ -100,8 +102,8 @@ t_admins_add_delete(_) ->
?assertEqual(1, length(Users)), ?assertEqual(1, length(Users)),
{ok, _} = emqx_dashboard_admin:change_password( {ok, _} = emqx_dashboard_admin:change_password(
<<"username">>, <<"username">>,
<<"password">>, <<"password_0">>,
<<"pwd">> <<"new_pwd_1234">>
), ),
timer:sleep(10), timer:sleep(10),
{ok, _} = emqx_dashboard_admin:remove_user(<<"username">>). {ok, _} = emqx_dashboard_admin:remove_user(<<"username">>).
@ -109,12 +111,12 @@ t_admins_add_delete(_) ->
t_admin_delete_self_failed(_) -> t_admin_delete_self_failed(_) ->
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
Desc = <<"simple description">>, Desc = <<"simple description">>,
_ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password">>, Desc), _ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password_1">>, Desc),
Admins = emqx_dashboard_admin:all_users(), Admins = emqx_dashboard_admin:all_users(),
?assertEqual(1, length(Admins)), ?assertEqual(1, length(Admins)),
Header = auth_header_(<<"username1">>, <<"password">>), Header = auth_header_(<<"username1">>, <<"password_1">>),
{error, {_, 400, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header), {error, {_, 400, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header),
Token = ["Basic ", base64:encode("username1:password")], Token = ["Basic ", base64:encode("username1:password_1")],
Header2 = {"Authorization", Token}, Header2 = {"Authorization", Token},
{error, {_, 401, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header2), {error, {_, 401, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header2),
mnesia:clear_table(?ADMIN). mnesia:clear_table(?ADMIN).
@ -122,7 +124,8 @@ t_admin_delete_self_failed(_) ->
t_rest_api(_Config) -> t_rest_api(_Config) ->
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
Desc = <<"administrator">>, Desc = <<"administrator">>,
emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, Desc), Password = <<"public_www1">>,
emqx_dashboard_admin:add_user(<<"admin">>, Password, Desc),
{ok, 200, Res0} = http_get(["users"]), {ok, 200, Res0} = http_get(["users"]),
?assertEqual( ?assertEqual(
[ [
@ -136,7 +139,7 @@ t_rest_api(_Config) ->
{ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}), {ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}),
{ok, 200, _} = http_post(["users"], #{ {ok, 200, _} = http_post(["users"], #{
<<"username">> => <<"usera">>, <<"username">> => <<"usera">>,
<<"password">> => <<"passwd">>, <<"password">> => <<"passwd_01234">>,
<<"description">> => Desc <<"description">> => Desc
}), }),
{ok, 204, _} = http_delete(["users", "usera"]), {ok, 204, _} = http_delete(["users", "usera"]),
@ -144,34 +147,34 @@ t_rest_api(_Config) ->
{ok, 204, _} = http_post( {ok, 204, _} = http_post(
["users", "admin", "change_pwd"], ["users", "admin", "change_pwd"],
#{ #{
<<"old_pwd">> => <<"public">>, <<"old_pwd">> => Password,
<<"new_pwd">> => <<"newpwd">> <<"new_pwd">> => <<"newpwd_lkdfki1">>
} }
), ),
mnesia:clear_table(?ADMIN), mnesia:clear_table(?ADMIN),
emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"administrator">>), emqx_dashboard_admin:add_user(<<"admin">>, Password, <<"administrator">>),
ok. ok.
t_cli(_Config) -> t_cli(_Config) ->
[mria:dirty_delete(?ADMIN, Admin) || Admin <- mnesia:dirty_all_keys(?ADMIN)], [mria:dirty_delete(?ADMIN, Admin) || Admin <- mnesia:dirty_all_keys(?ADMIN)],
emqx_dashboard_cli:admins(["add", "username", "password"]), emqx_dashboard_cli:admins(["add", "username", "password_ww2"]),
[#?ADMIN{username = <<"username">>, pwdhash = <<Salt:4/binary, Hash/binary>>}] = [#?ADMIN{username = <<"username">>, pwdhash = <<Salt:4/binary, Hash/binary>>}] =
emqx_dashboard_admin:lookup_user(<<"username">>), emqx_dashboard_admin:lookup_user(<<"username">>),
?assertEqual(Hash, crypto:hash(sha256, <<Salt/binary, <<"password">>/binary>>)), ?assertEqual(Hash, crypto:hash(sha256, <<Salt/binary, <<"password_ww2">>/binary>>)),
emqx_dashboard_cli:admins(["passwd", "username", "newpassword"]), emqx_dashboard_cli:admins(["passwd", "username", "new_password"]),
[#?ADMIN{username = <<"username">>, pwdhash = <<Salt1:4/binary, Hash1/binary>>}] = [#?ADMIN{username = <<"username">>, pwdhash = <<Salt1:4/binary, Hash1/binary>>}] =
emqx_dashboard_admin:lookup_user(<<"username">>), emqx_dashboard_admin:lookup_user(<<"username">>),
?assertEqual(Hash1, crypto:hash(sha256, <<Salt1/binary, <<"newpassword">>/binary>>)), ?assertEqual(Hash1, crypto:hash(sha256, <<Salt1/binary, <<"new_password">>/binary>>)),
emqx_dashboard_cli:admins(["del", "username"]), emqx_dashboard_cli:admins(["del", "username"]),
[] = emqx_dashboard_admin:lookup_user(<<"username">>), [] = emqx_dashboard_admin:lookup_user(<<"username">>),
emqx_dashboard_cli:admins(["add", "admin1", "pass1"]), emqx_dashboard_cli:admins(["add", "admin1", "pass_lkdfkd1"]),
emqx_dashboard_cli:admins(["add", "admin2", "passw2"]), emqx_dashboard_cli:admins(["add", "admin2", "w_pass_lkdfkd2"]),
AdminList = emqx_dashboard_admin:all_users(), AdminList = emqx_dashboard_admin:all_users(),
?assertEqual(2, length(AdminList)). ?assertEqual(2, length(AdminList)).
t_lookup_by_username_jwt(_Config) -> t_lookup_by_username_jwt(_Config) ->
User = bin(["user-", integer_to_list(random_num())]), User = bin(["user-", integer_to_list(random_num())]),
Pwd = bin(integer_to_list(random_num())), Pwd = bin("t_password" ++ integer_to_list(random_num())),
emqx_dashboard_token:sign(User, Pwd), emqx_dashboard_token:sign(User, Pwd),
?assertMatch( ?assertMatch(
[#?ADMIN_JWT{username = User}], [#?ADMIN_JWT{username = User}],
@ -185,7 +188,7 @@ t_lookup_by_username_jwt(_Config) ->
t_clean_expired_jwt(_Config) -> t_clean_expired_jwt(_Config) ->
User = bin(["user-", integer_to_list(random_num())]), User = bin(["user-", integer_to_list(random_num())]),
Pwd = bin(integer_to_list(random_num())), Pwd = bin("t_password" ++ integer_to_list(random_num())),
emqx_dashboard_token:sign(User, Pwd), emqx_dashboard_token:sign(User, Pwd),
[#?ADMIN_JWT{username = User, exptime = ExpTime}] = [#?ADMIN_JWT{username = User, exptime = ExpTime}] =
emqx_dashboard_token:lookup_by_username(User), emqx_dashboard_token:lookup_by_username(User),
@ -208,16 +211,16 @@ random_num() ->
erlang:system_time(nanosecond). erlang:system_time(nanosecond).
http_get(Parts) -> http_get(Parts) ->
request_api(get, api_path(Parts), auth_header_()). request_api(get, api_path(Parts), auth_header_(<<"admin">>, <<"public_www1">>)).
http_delete(Parts) -> http_delete(Parts) ->
request_api(delete, api_path(Parts), auth_header_()). request_api(delete, api_path(Parts), auth_header_(<<"admin">>, <<"public_www1">>)).
http_post(Parts, Body) -> http_post(Parts, Body) ->
request_api(post, api_path(Parts), [], auth_header_(), Body). request_api(post, api_path(Parts), [], auth_header_(<<"admin">>, <<"public_www1">>), Body).
http_put(Parts, Body) -> http_put(Parts, Body) ->
request_api(put, api_path(Parts), [], auth_header_(), Body). request_api(put, api_path(Parts), [], auth_header_(<<"admin">>, <<"public_www1">>), Body).
request_dashboard(Method, Url, Auth) -> request_dashboard(Method, Url, Auth) ->
Request = {Url, [Auth]}, Request = {Url, [Auth]},

View File

@ -51,7 +51,7 @@ end_suite() ->
t_check_user(_) -> t_check_user(_) ->
Username = <<"admin1">>, Username = <<"admin1">>,
Password = <<"public">>, Password = <<"public_1">>,
BadUsername = <<"admin_bad">>, BadUsername = <<"admin_bad">>,
BadPassword = <<"public_bad">>, BadPassword = <<"public_bad">>,
EmptyUsername = <<>>, EmptyUsername = <<>>,
@ -108,7 +108,7 @@ t_lookup_user(_) ->
t_all_users(_) -> t_all_users(_) ->
Username = <<"admin_all">>, Username = <<"admin_all">>,
Password = <<"public">>, Password = <<"public_2">>,
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>),
All = emqx_dashboard_admin:all_users(), All = emqx_dashboard_admin:all_users(),
?assert(erlang:length(All) >= 1), ?assert(erlang:length(All) >= 1),
@ -153,6 +153,7 @@ t_change_password(_) ->
Description = <<"change_description">>, Description = <<"change_description">>,
NewPassword = <<"new_password">>, NewPassword = <<"new_password">>,
NewBadPassword = <<"public">>,
BadChangeUser = <<"change_user_bad">>, BadChangeUser = <<"change_user_bad">>,
@ -163,14 +164,17 @@ t_change_password(_) ->
{error, <<"password_error">>} = {error, <<"password_error">>} =
emqx_dashboard_admin:change_password(User, OldPassword, NewPassword), emqx_dashboard_admin:change_password(User, OldPassword, NewPassword),
{error, <<"The range of password length is 8~64">>} =
emqx_dashboard_admin:change_password(User, NewPassword, NewBadPassword),
{error, <<"username_not_found">>} = {error, <<"username_not_found">>} =
emqx_dashboard_admin:change_password(BadChangeUser, OldPassword, NewPassword), emqx_dashboard_admin:change_password(BadChangeUser, OldPassword, NewPassword),
ok. ok.
t_clean_token(_) -> t_clean_token(_) ->
Username = <<"admin_token">>, Username = <<"admin_token">>,
Password = <<"public">>, Password = <<"public_www1">>,
NewPassword = <<"public1">>, NewPassword = <<"public_www2">>,
{ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>),
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
ok = emqx_dashboard_admin:verify_token(Token), ok = emqx_dashboard_admin:verify_token(Token),

View File

@ -33,6 +33,7 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
application:load(emqx_dashboard),
mria:start(), mria:start(),
emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1), emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1),
Config. Config.

View File

@ -0,0 +1,3 @@
Add a password complexity requirement when adding or modifying Dashboard users via the API.
Now password must contain at least 2 of alphabetic, numeric and special characters,
and must be 8 to 64 characters long.

View File

@ -0,0 +1,2 @@
通过 API 添加、修改 Dashboard 用户时,增加对密码复杂度的要求。
现在密码必须包含字母、数字以及特殊字符中的至少 2 种,并且长度范围必须是 8~64 个字符。