diff options
author | Christopher Astfalk <christopher.astfalk@icloud.com> | 2021-03-29 22:06:20 +0200 |
---|---|---|
committer | Bessenyei Balázs Donát <bessbd@users.noreply.github.com> | 2021-05-05 10:31:38 +0200 |
commit | ba8b1674f04ce08f9166dbb5820a7f8d3f7943c2 (patch) | |
tree | 55ccbb0118f14b3e180519837ea95e6cb562e14d | |
parent | 19204484c56f043514376721caafe74fd7ad5c74 (diff) | |
download | couchdb-ba8b1674f04ce08f9166dbb5820a7f8d3f7943c2.tar.gz |
Add password validation
-rw-r--r-- | rel/overlay/etc/default.ini | 4 | ||||
-rw-r--r-- | src/fabric/src/fabric2_users_db.erl | 80 | ||||
-rw-r--r-- | test/elixir/test/users_db_test.exs | 98 |
3 files changed, 168 insertions, 14 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index e267b4a30..3d15eb48a 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -183,6 +183,10 @@ iterations = 10 ; iterations for password hashing ; min_iterations = 1 ; max_iterations = 1000000000 ; password_scheme = pbkdf2 +; List of Erlang RegExp or tuples of RegExp and an optional error message. +; Where a new password must match all RegExp. +; Example: [{".{10,}", "Password min length is 10 characters."}, "\\d+"] +; password_regexp = [] ; proxy_use_secret = false ; comma-separated list of public fields, 404 if empty ; public_fields = diff --git a/src/fabric/src/fabric2_users_db.erl b/src/fabric/src/fabric2_users_db.erl index fdc787a02..3714d341e 100644 --- a/src/fabric/src/fabric2_users_db.erl +++ b/src/fabric/src/fabric2_users_db.erl @@ -31,6 +31,8 @@ -define(ITERATIONS, <<"iterations">>). -define(SALT, <<"salt">>). -define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})). +-define(REQUIREMENT_ERROR, "Password does not conform to requirements."). +-define(PASSWORD_SERVER_ERROR, "Server cannot hash passwords at this time."). -define( DDOCS_ADMIN_ONLY, @@ -77,6 +79,7 @@ save_doc(#doc{body={Body}} = Doc) -> {undefined, _} -> Doc; {ClearPassword, "simple"} -> % deprecated + ok = validate_password(ClearPassword), Salt = couch_uuids:random(), PasswordSha = couch_passwords:simple(ClearPassword, Salt), Body0 = ?replace(Body, ?PASSWORD_SCHEME, ?SIMPLE), @@ -85,6 +88,7 @@ save_doc(#doc{body={Body}} = Doc) -> Body3 = proplists:delete(?PASSWORD, Body2), Doc#doc{body={Body3}}; {ClearPassword, "pbkdf2"} -> + ok = validate_password(ClearPassword), Iterations = list_to_integer(config:get("couch_httpd_auth", "iterations", "1000")), Salt = couch_uuids:random(), DerivedKey = couch_passwords:pbkdf2(ClearPassword, Salt, Iterations), @@ -103,7 +107,81 @@ save_doc(#doc{body={Body}} = Doc) -> details => "password_scheme must one of (simple, pbkdf2)" }), couch_log:error("[couch_httpd_auth] password_scheme value of '~p' is invalid.", [Scheme]), - throw({forbidden, "Server cannot hash passwords at this time."}) + throw({forbidden, ?PASSWORD_SERVER_ERROR}) + end. + +% Validate if a new password matches all RegExp in the password_regexp setting. +% Throws if not. +% In this function the [couch_httpd_auth] password_regexp config is parsed. +validate_password(ClearPassword) -> + case config:get("couch_httpd_auth", "password_regexp", "") of + "" -> + ok; + "[]" -> + ok; + ValidateConfig -> + RequirementList = case couch_util:parse_term(ValidateConfig) of + {ok, RegExpList} when is_list(RegExpList) -> + RegExpList; + {ok, NonListValue} -> + couch_log:error( + "[couch_httpd_auth] password_regexp value of '~p'" + " is not a list.", + [NonListValue] + ), + throw({forbidden, ?PASSWORD_SERVER_ERROR}); + {error, ErrorInfo} -> + couch_log:error( + "[couch_httpd_auth] password_regexp value of '~p'" + " could not get parsed. ~p", + [ValidateConfig, ErrorInfo] + ), + throw({forbidden, ?PASSWORD_SERVER_ERROR}) + end, + % Check the password on every RegExp. + lists:foreach(fun(RegExpTuple) -> + case get_password_regexp_and_error_msg(RegExpTuple) of + {ok, RegExp, PasswordErrorMsg} -> + check_password(ClearPassword, RegExp, PasswordErrorMsg); + {error} -> + couch_log:error( + "[couch_httpd_auth] password_regexp part of '~p' " + "is not a RegExp string or " + "a RegExp and Reason tuple.", + [RegExpTuple] + ), + throw({forbidden, ?PASSWORD_SERVER_ERROR}) + end + end, RequirementList), + ok + end. + +% Get the RegExp out of the tuple and combine the the error message. +% First is with a Reason string. +get_password_regexp_and_error_msg({RegExp, Reason}) + when is_list(RegExp) andalso is_list(Reason) + andalso length(Reason) > 0 -> + {ok, RegExp, lists:concat([?REQUIREMENT_ERROR, " ", Reason])}; +% With a not correct Reason string. +get_password_regexp_and_error_msg({RegExp, _Reason}) when is_list(RegExp) -> + {ok, RegExp, ?REQUIREMENT_ERROR}; +% Without a Reason string. +get_password_regexp_and_error_msg({RegExp}) when is_list(RegExp) -> + {ok, RegExp, ?REQUIREMENT_ERROR}; +% If the RegExp is only a list/string. +get_password_regexp_and_error_msg(RegExp) when is_list(RegExp) -> + {ok, RegExp, ?REQUIREMENT_ERROR}; +% Not correct RegExpValue. +get_password_regexp_and_error_msg(_) -> + {error}. + +% Check the password if it matches a RegExp. +check_password(Password, RegExp, ErrorMsg) -> + case re:run(Password, RegExp, [{capture, none}]) of + match -> + ok; + _ -> + throw({bad_request, ErrorMsg}) end. diff --git a/test/elixir/test/users_db_test.exs b/test/elixir/test/users_db_test.exs index 4ef6d5371..34fae198b 100644 --- a/test/elixir/test/users_db_test.exs +++ b/test/elixir/test/users_db_test.exs @@ -300,11 +300,21 @@ defmodule UsersDbTest do assert resp.body["userCtx"]["name"] == "foo@example.org" end + @tag :with_db test "users password requirements", context do set_config({ "couch_httpd_auth", - "password_reqexp", - "[{\".{10,}\"}, {\"[A-Z]+\", \"Requirement 2.\"}, {\"[a-z]+\", \"\"}, {\"\\\\d+\", \"Req 4.\"}]" + "password_regexp", + Enum.join( + [ + "[{\".{10,}\"},", # 10 chars + "{\"[A-Z]+\", \"Requirement 2.\"},", # a uppercase char + "{\"[a-z]+\", \"\"},", # a lowercase char + "{\"\\\\d+\", \"Req 4.\"},", # A number + "\"[!\.,\(\)]+\"]" # A special char + ], + " " + ) }) session = login("jan", "apple") @@ -320,8 +330,8 @@ defmodule UsersDbTest do @users_db_name, jchris_user_doc, use_session: session, - expect_response: 403, - error_message: "forbidden", + expect_response: 400, + error_message: "bad_request", error_reason: "Password does not conform to requirements." ) @@ -332,8 +342,8 @@ defmodule UsersDbTest do @users_db_name, jchris_user_doc2, use_session: session, - expect_response: 403, - error_message: "forbidden", + expect_response: 400, + error_message: "bad_request", error_reason: "Password does not conform to requirements. Requirement 2." ) @@ -344,25 +354,87 @@ defmodule UsersDbTest do @users_db_name, jchris_user_doc3, use_session: session, - expect_response: 403, - error_message: "forbidden", + expect_response: 400, + error_message: "bad_request", error_reason: "Password does not conform to requirements." ) - # With password that match all but the last requirements. + # With password that match the first three requirements. # Requirement does have a reason text. jchris_user_doc4 = Map.put(jchris_user_doc, "password", "funnnnnyBONE") save_as( @users_db_name, jchris_user_doc4, use_session: session, - expect_response: 403, - error_message: "forbidden", + expect_response: 400, + error_message: "bad_request", error_reason: "Password does not conform to requirements. Req 4." ) - # With password that match all requirements. + # With password that match all but the last requirements. + # Requirement does have a reason text. jchris_user_doc5 = Map.put(jchris_user_doc, "password", "funnnnnyB0N3") - save_as(@users_db_name, jchris_user_doc5, use_session: session, expect_response: 201) + save_as( + @users_db_name, + jchris_user_doc5, + use_session: session, + expect_response: 400, + error_message: "bad_request", + error_reason: "Password does not conform to requirements." + ) + + # With password that match all requirements. + jchris_user_doc6 = Map.put(jchris_user_doc, "password", "funnnnnyB0N3!") + save_as(@users_db_name, jchris_user_doc6, use_session: session, expect_response: 201) + end + + @tag :with_db + test "users password requirements with non list value", context do + set_config({ + "couch_httpd_auth", + "password_regexp", + "{{\".{10,}\"}}" + }) + + session = login("jan", "apple") + + jchris_user_doc = + prepare_user_doc([ + {:name, "jchris@apache.org"}, + {:password, "funnybone"} + ]) + save_as( + @users_db_name, + jchris_user_doc, + use_session: session, + expect_response: 403, + error_message: "forbidden", + error_reason: "Server cannot hash passwords at this time." + ) + end + + @tag :with_db + test "users password requirements with not correct syntax", context do + set_config({ + "couch_httpd_auth", + "password_regexp", + "[{\".{10,}\"]" + }) + + session = login("jan", "apple") + + jchris_user_doc = + prepare_user_doc([ + {:name, "jchris@apache.org"}, + {:password, "funnybone"} + ]) + save_as( + @users_db_name, + jchris_user_doc, + use_session: session, + expect_response: 403, + error_message: "forbidden", + error_reason: "Server cannot hash passwords at this time." + ) end end |