Просмотр исходного кода

feat: add dashboard password validate

Zhongwen Deng 3 лет назад
Родитель
Сommit
a523fa2fa2

+ 112 - 12
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.
+
+-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.
 
-%% 0 - 9 or A -Z or a - z or $_
-legal_username(<<>>) -> false;
-legal_username(UserName) -> nomatch /= re:run(UserName, "^[_a-zA-Z0-9]*$").
+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.

+ 8 - 4
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),