blob: bc09df9ba6c7959ffa5be81acc16abe625330290 [file] [log] [blame]
defmodule UsersDbTest do
use CouchTestCase
@moduletag :authentication
@users_db_name "_users"
@moduletag config: [
{
"chttpd_auth",
"authentication_db",
@users_db_name
},
{
"couch_httpd_auth",
"authentication_db",
@users_db_name
},
{
"chttpd_auth",
"iterations",
"1"
},
{
"admins",
"jan",
"apple"
}
]
setup do
reset_db(@users_db_name)
wait_for_design_auth(@users_db_name)
on_exit(&tear_down/0)
end
defp tear_down do
reset_db(@users_db_name)
end
defp save_as(db_name, doc, options) do
session = Keyword.get(options, :use_session)
expect_response = Keyword.get(options, :expect_response, [201, 202])
expect_message = Keyword.get(options, :error_message)
expect_reason = Keyword.get(options, :error_reason)
headers =
if session != nil do
[
Cookie: session.cookie,
"X-CouchDB-www-Authenticate": "Cookie"
]
else
[]
end
resp =
Couch.put(
"/#{db_name}/#{URI.encode(doc["_id"])}",
headers: headers,
body: doc
)
if is_list(expect_response) do
assert resp.status_code in expect_response
else
assert resp.status_code == expect_response
end
if expect_message != nil do
assert resp.body["error"] == expect_message
end
if expect_reason != nil do
assert resp.body["reason"] == expect_reason
end
resp
end
defp login(user, password) do
sess = Couch.login(user, password)
assert sess.cookie, "Login correct is expected"
sess
end
defp logout(session) do
assert Couch.Session.logout(session).body["ok"]
end
@tag :with_db
test "users db", context do
db_name = context[:db_name]
# test that the users db is born with the auth ddoc
get_ddoc = fn ->
ddoc = Couch.get("/#{@users_db_name}/_design/_auth")
ddoc.body["validate_doc_update"]
end
retry_until(fn -> get_ddoc.() != nil end)
assert get_ddoc.() != nil
jchris_user_doc =
prepare_user_doc([
{:name, "jchris@apache.org"},
{:password, "funnybone"}
])
{:ok, resp} = create_doc(@users_db_name, jchris_user_doc)
jchris_rev = resp.body["rev"]
resp =
Couch.get(
"/_session",
headers: [authorization: "Basic #{:base64.encode("jchris@apache.org:funnybone")}"]
)
assert resp.body["userCtx"]["name"] == "jchris@apache.org"
assert resp.body["info"]["authenticated"] == "default"
assert resp.body["info"]["authentication_db"] == @users_db_name
assert Enum.member?(resp.body["info"]["authentication_handlers"], "cookie")
assert Enum.member?(resp.body["info"]["authentication_handlers"], "default")
resp =
Couch.get(
"/_session",
headers: [authorization: "Basic Xzpf"]
)
assert resp.body["userCtx"]["name"] == :null
assert not Enum.member?(resp.body["info"], "authenticated")
# ok, now create a conflicting edit on the jchris doc, and make sure there's no login.
# (use replication to create the conflict) - need 2 be admin
session = login("jan", "apple")
replicate(@users_db_name, db_name)
jchris_user_doc = Map.put(jchris_user_doc, "_rev", jchris_rev)
jchris_user_doc2 = Map.put(jchris_user_doc, "foo", "bar")
save_as(@users_db_name, jchris_user_doc2, use_session: session)
save_as(@users_db_name, jchris_user_doc, use_session: session, expect_response: 409)
# then in the other
jchris_user_doc3 = Map.put(jchris_user_doc, "foo", "barrrr")
save_as(db_name, jchris_user_doc3, use_session: session)
replicate(db_name, @users_db_name)
# now we should have a conflict
resp =
Couch.get(
"/#{@users_db_name}/#{jchris_user_doc3["_id"]}",
query: [conflicts: true]
)
assert length(resp.body["_conflicts"]) == 1
jchris_with_conflict = resp.body
logout(session)
# wait for auth_cache invalidation
retry_until(
fn ->
resp =
Couch.get(
"/_session",
headers: [
authorization: "Basic #{:base64.encode("jchris@apache.org:funnybone")}"
]
)
assert resp.body["error"] == "unauthorized"
assert String.contains?(resp.body["reason"], "conflict")
resp
end,
500,
20_000
)
# You can delete a user doc
session = login("jan", "apple")
info = Couch.Session.info(session)
assert Enum.member?(info["userCtx"]["roles"], "_admin")
resp =
Couch.delete(
"/#{@users_db_name}/#{jchris_with_conflict["_id"]}",
query: [rev: jchris_with_conflict["_rev"]],
headers: [
Cookie: session.cookie,
"X-CouchDB-www-Authenticate": "Cookie"
]
)
assert resp.body["ok"]
# you can't change doc from type "user"
resp =
Couch.get(
"/#{@users_db_name}/#{jchris_user_doc["_id"]}",
headers: [
Cookie: session.cookie,
"X-CouchDB-www-Authenticate": "Cookie"
]
)
assert resp.status_code == 200
jchris_user_doc = Map.replace!(resp.body, "type", "not user")
save_as(
@users_db_name,
jchris_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "doc.type must be user"
)
# "roles" must be an array
jchris_user_doc =
jchris_user_doc
|> Map.replace!("type", "user")
|> Map.replace!("roles", "not an array")
save_as(
@users_db_name,
jchris_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "doc.roles must be an array"
)
# "roles" must be and array of strings
jchris_user_doc = Map.replace!(jchris_user_doc, "roles", [12])
save_as(
@users_db_name,
jchris_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "doc.roles can only contain strings"
)
# "roles" must exist
jchris_user_doc = Map.drop(jchris_user_doc, ["roles"])
save_as(
@users_db_name,
jchris_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "doc.roles must exist"
)
# character : is not allowed in usernames
joe_user_doc =
prepare_user_doc([
{:name, "joe:erlang"},
{:password, "querty"}
])
save_as(
@users_db_name,
joe_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "Character `:` is not allowed in usernames."
)
# test that you can login as a user with a password starting with :
joe_user_doc =
prepare_user_doc([
{:name, "foo@example.org"},
{:password, ":bar"}
])
{:ok, _} = create_doc(@users_db_name, joe_user_doc)
logout(session)
resp =
Couch.get(
"/_session",
headers: [authorization: "Basic #{:base64.encode("foo@example.org::bar")}"]
)
assert resp.body["userCtx"]["name"] == "foo@example.org"
end
test "users password requirements", _context do
set_config({
"couch_httpd_auth",
"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")
# With password that doesn't confirm to any requirement.
# Requirement doesn't have a reason text.
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: 400,
error_message: "bad_request",
error_reason: "Password does not conform to requirements."
)
# With password that match the first requirement.
# Requirement does have a reason text.
jchris_user_doc2 = Map.put(jchris_user_doc, "password", "funnnnnybone")
save_as(
@users_db_name,
jchris_user_doc2,
use_session: session,
expect_response: 400,
error_message: "bad_request",
error_reason: "Password does not conform to requirements. Requirement 2."
)
# With password that match the first two requirements.
# Requirement does have an empty string as reason text.
jchris_user_doc3 = Map.put(jchris_user_doc, "password", "FUNNNNNYBONE")
save_as(
@users_db_name,
jchris_user_doc3,
use_session: session,
expect_response: 400,
error_message: "bad_request",
error_reason: "Password does not conform to 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: 400,
error_message: "bad_request",
error_reason: "Password does not conform to requirements. Req 4."
)
# 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: 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)
# with non list value
set_config({
"couch_httpd_auth",
"password_regexp",
"{{\".{10,}\"}}"
})
joe_user_doc =
prepare_user_doc([
{:name, "joe_erlang"},
{:password, "querty"}
])
save_as(
@users_db_name,
joe_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "Server cannot hash passwords at this time."
)
# Not correct syntax
set_config({
"couch_httpd_auth",
"password_regexp",
"[{\".{10,}\"]"
})
save_as(
@users_db_name,
joe_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden",
error_reason: "Server cannot hash passwords at this time."
)
end
end