summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristopher Astfalk <christopher.astfalk@icloud.com>2021-03-29 22:06:20 +0200
committerBessenyei Balázs Donát <bessbd@users.noreply.github.com>2021-05-05 10:31:38 +0200
commitba8b1674f04ce08f9166dbb5820a7f8d3f7943c2 (patch)
tree55ccbb0118f14b3e180519837ea95e6cb562e14d
parent19204484c56f043514376721caafe74fd7ad5c74 (diff)
downloadcouchdb-ba8b1674f04ce08f9166dbb5820a7f8d3f7943c2.tar.gz
Add password validation
-rw-r--r--rel/overlay/etc/default.ini4
-rw-r--r--src/fabric/src/fabric2_users_db.erl80
-rw-r--r--test/elixir/test/users_db_test.exs98
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