From a523fa2fa2e211b3b2109a8f1c623583336252ed Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 16 Jan 2023 17:33:28 +0800 Subject: [PATCH] feat: add dashboard password validate --- .../src/emqx_dashboard_admin.erl | 124 ++++++++++++++++-- .../test/emqx_dashboard_admin_SUITE.erl | 12 +- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index e36c2628b..b4f0d7d4a 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -92,21 +92,79 @@ add_default_user() -> add_user(Username, Password, Desc) when is_binary(Username), is_binary(Password) -> - case legal_username(Username) of - true -> - return( - mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]) - ); - false -> + case {legal_username(Username), legal_password(Password)} of + {ok, ok} -> do_add_user(Username, Password, Desc); + {{error, Reason}, _} -> {error, Reason}; + {_, {error, Reason}} -> {error, Reason} + end. + +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 can not be empty">>}; +legal_username(UserName) -> + case re:run(UserName, "^[_a-zA-Z0-9]*$", [{capture, none}]) of + nomatch -> {error, << "Bad Username." " Only upper and lower case letters, numbers and underscores are supported" - >>} + >>}; + match -> + ok end. -%% 0 - 9 or A -Z or a - z or $_ -legal_username(<<>>) -> false; -legal_username(UserName) -> nomatch /= re:run(UserName, "^[_a-zA-Z0-9]*$"). +-define(LOW_LETTER_CHARS, "abcdefghijklmnopqrstuvwxyz"). +-define(UPPER_LETTER_CHARS, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"). +-define(LETTER, ?LOW_LETTER_CHARS ++ ?UPPER_LETTER_CHARS). +-define(NUMBER, "0123456789"). +-define(SPECIAL_CHARS, "!@#$%^&*()_+-=[]{}\"|;':,./<>?`~ "). +-define(INVALID_PASSWORD_MSG, << + "Bad username." + "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 force_add_user(Username, Password, Desc) -> @@ -188,7 +246,10 @@ change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) -> end. 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) -> ChangePWD = @@ -292,6 +353,45 @@ add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY {ok, empty}; add_default_user(Username, Password) -> case lookup_user(Username) of - [] -> add_user(Username, Password, <<"administrator">>); + [] -> do_add_user(Username, Password, <<"administrator">>); _ -> {ok, default_user_exists} 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. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index fefc492cc..9ae5d4418 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -51,7 +51,7 @@ end_suite() -> t_check_user(_) -> Username = <<"admin1">>, - Password = <<"public">>, + Password = <<"public_1">>, BadUsername = <<"admin_bad">>, BadPassword = <<"public_bad">>, EmptyUsername = <<>>, @@ -108,7 +108,7 @@ t_lookup_user(_) -> t_all_users(_) -> Username = <<"admin_all">>, - Password = <<"public">>, + Password = <<"public_2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), All = emqx_dashboard_admin:all_users(), ?assert(erlang:length(All) >= 1), @@ -153,6 +153,7 @@ t_change_password(_) -> Description = <<"change_description">>, NewPassword = <<"new_password">>, + NewBadPassword = <<"public">>, BadChangeUser = <<"change_user_bad">>, @@ -163,14 +164,17 @@ t_change_password(_) -> {error, <<"password_error">>} = 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">>} = emqx_dashboard_admin:change_password(BadChangeUser, OldPassword, NewPassword), ok. t_clean_token(_) -> Username = <<"admin_token">>, - Password = <<"public">>, - NewPassword = <<"public1">>, + Password = <<"public_www1">>, + NewPassword = <<"public_www2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), ok = emqx_dashboard_admin:verify_token(Token),