blob: cf9b36c7f3a361a50be7a7dba95d247b76a7e4fb [file] [log] [blame]
defmodule CookieAuthTest do
use CouchTestCase
@moduletag :authentication
@users_db "_users"
@moduletag config: [
{
"chttpd_auth",
"authentication_db",
@users_db
},
{
"couch_httpd_auth",
"authentication_db",
@users_db
},
{
"chttpd_auth",
"iterations",
"1"
},
{
"admins",
"jan",
"apple"
}
]
@password "3.141592653589"
setup do
reset_db(@users_db)
wait_for_design_auth(@users_db)
on_exit(&tear_down/0)
end
defp tear_down do
reset_db(@users_db)
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
defp login_as(user) do
pws = %{
"jan" => "apple",
"Jason Davies" => @password,
"jchris" => "funnybone"
}
user1 = Regex.replace(~r/[0-9]$/, user, "")
login(user1, pws[user])
end
defp create_doc_expect_error(db_name, doc, status_code, msg) do
resp = Couch.post("/#{db_name}", body: doc)
assert resp.status_code == status_code
assert resp.body["error"] == msg
resp
end
defp open_as(db_name, doc_id, options) do
use_session = Keyword.get(options, :use_session)
user = Keyword.get(options, :user)
expect_response = Keyword.get(options, :expect_response, 200)
expect_message = Keyword.get(options, :error_message)
session = use_session || login_as(user)
resp =
Couch.Session.get(
session,
"/#{db_name}/#{URI.encode(doc_id)}"
)
if use_session == nil do
logout(session)
end
assert resp.status_code == expect_response
if expect_message != nil do
assert resp.body["error"] == expect_message
end
resp.body
end
defp save_as(db_name, doc, options) do
use_session = Keyword.get(options, :use_session)
user = Keyword.get(options, :user)
expect_response = Keyword.get(options, :expect_response, [201, 202])
expect_message = Keyword.get(options, :error_message)
session = use_session || login_as(user)
resp =
Couch.Session.put(
session,
"/#{db_name}/#{URI.encode(doc["_id"])}",
body: doc
)
if use_session == nil do
logout(session)
end
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
resp
end
defp delete_as(db_name, doc, options) do
use_session = Keyword.get(options, :use_session)
user = Keyword.get(options, :user)
expect_response = Keyword.get(options, :expect_response, [200, 202])
expect_message = Keyword.get(options, :error_message)
session = use_session || login_as(user)
resp =
Couch.Session.delete(
session,
"/#{db_name}/#{URI.encode(doc["_id"])}"
)
if use_session == nil do
logout(session)
end
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
resp
end
defp test_change_admin_fun do
sess = login("jchris", "funnybone")
info = Couch.Session.info(sess)
assert info["userCtx"]["name"] == "jchris"
assert Enum.member?(info["userCtx"]["roles"], "_admin")
assert Enum.member?(info["userCtx"]["roles"], "foo")
jchris_user_doc =
open_as(
@users_db,
"org.couchdb.user:jchris",
use_session: sess
)
jchris_user_doc = Map.drop(jchris_user_doc, [:salt, :password_sha])
save_as(@users_db, jchris_user_doc, use_session: sess)
logout(sess)
sess = login("jchris", "funnybone")
info = Couch.Session.info(sess)
assert info["userCtx"]["name"] == "jchris"
assert Enum.member?(info["userCtx"]["roles"], "_admin")
assert info["info"]["authenticated"] == "cookie"
assert info["info"]["authentication_db"] == @users_db
assert Enum.member?(info["userCtx"]["roles"], "foo")
logout(sess)
end
test "cookie auth" do
# test that the users db is born with the auth ddoc
ddoc = open_as(@users_db, "_design/_auth", user: "jan")
assert ddoc["validate_doc_update"] != nil
jason_user_doc =
prepare_user_doc([
{:name, "Jason Davies"},
{:password, @password}
])
create_doc(@users_db, jason_user_doc)
jason_check_doc = open_as(@users_db, jason_user_doc["_id"], user: "jan")
assert jason_check_doc["name"] == "Jason Davies"
jchris_user_doc =
prepare_user_doc([
{:name, "jchris"},
{:password, "funnybone"}
])
{:ok, resp} = create_doc(@users_db, jchris_user_doc)
jchris_rev = resp.body["rev"]
duplicate_jchris_user_doc =
prepare_user_doc([
{:name, "jchris"},
{:password, "eh, Boo-Boo?"}
])
# make sure we can't create duplicate users
create_doc_expect_error(@users_db, duplicate_jchris_user_doc, 409, "conflict")
# we can't create _names
underscore_user_doc =
prepare_user_doc([
{:name, "_why"},
{:password, "copperfield"}
])
create_doc_expect_error(@users_db, underscore_user_doc, 403, "forbidden")
# we can't create malformed ids
bad_id_user_doc =
prepare_user_doc([
{:id, "org.apache.couchdb:w00x"},
{:name, "w00x"},
{:password, "bar"}
])
create_doc_expect_error(@users_db, bad_id_user_doc, 403, "forbidden")
# login works
session = login_as("Jason Davies")
info = Couch.Session.info(session)
assert info["userCtx"]["name"] == "Jason Davies"
assert not Enum.member?(info["userCtx"]["roles"], "_admin")
# update one's own credentials document
jason_user_doc =
jason_user_doc
|> Map.put("_rev", jason_check_doc["_rev"])
|> Map.put("foo", 2)
resp = save_as(@users_db, jason_user_doc, use_session: session)
jason_user_doc_rev = resp.body["rev"]
# can't delete another users doc unless you are admin
jchris_user_doc = Map.put(jchris_user_doc, "_rev", jchris_rev)
delete_as(
@users_db,
jchris_user_doc,
use_session: session,
expect_response: 404,
error_message: "not_found"
)
logout(session)
# test redirect on success
resp =
Couch.post(
"/_session",
query: [next: "/_up"],
body: %{
:username => "Jason Davies",
:password => @password
}
)
assert resp.status_code == 302
assert resp.body["ok"]
assert String.ends_with?(resp.headers["location"], "/_up")
# test redirect on fail
resp =
Couch.post(
"/_session",
query: [fail: "/_up"],
body: %{
:username => "Jason Davies",
:password => "foobar"
}
)
assert resp.status_code == 302
assert resp.body["error"] == "unauthorized"
assert String.ends_with?(resp.headers["location"], "/_up")
session = login("jchris", "funnybone")
info = Couch.Session.info(session)
assert info["userCtx"]["name"] == "jchris"
assert Enum.empty?(info["userCtx"]["roles"])
jason_user_doc =
jason_user_doc
|> Map.put("_rev", jason_user_doc_rev)
|> Map.put("foo", 3)
save_as(
@users_db,
jason_user_doc,
use_session: session,
expect_response: 404,
error_message: "not_found"
)
jchris_user_doc = Map.put(jchris_user_doc, "roles", ["foo"])
save_as(
@users_db,
jchris_user_doc,
use_session: session,
expect_response: 403,
error_message: "forbidden"
)
logout(session)
jchris_user_doc = Map.put(jchris_user_doc, "foo", ["foo"])
resp =
save_as(
@users_db,
jchris_user_doc,
user: "jan"
)
# test that you can't save system (underscore) roles even if you are admin
jchris_user_doc =
jchris_user_doc
|> Map.put("roles", ["_bar"])
|> Map.put("_rev", resp.body["rev"])
save_as(
@users_db,
jchris_user_doc,
user: "jan",
expect_response: 403,
error_message: "forbidden"
)
session = login("jchris", "funnybone")
info = Couch.Session.info(session)
assert not Enum.member?(info["userCtx"]["roles"], "_admin")
assert(Enum.member?(info["userCtx"]["roles"], "foo"))
logout(session)
login("jan", "apple")
run_on_modified_server(
[
%{
:section => "admins",
:key => "jchris",
:value => "funnybone"
}
],
&test_change_admin_fun/0
)
# log in one last time so run_on_modified_server can clean up the admin account
login("jan", "apple")
end
test "basic+cookie auth interaction" do
# performing a successful basic authentication will create a session cookie
resp = Couch.get(
"/_all_dbs",
no_auth: true,
headers: [authorization: "Basic #{:base64.encode("jan:apple")}"])
assert resp.status_code == 200
# extract cookie value
cookie = resp.headers[:"set-cookie"]
[token | _] = String.split(cookie, ";")
# Cookie is usable on its own
resp = Couch.get(
"/_session",
no_auth: true,
headers: [cookie: token])
assert resp.status_code == 200
assert resp.body["userCtx"]["name"] == "jan"
assert resp.body["info"]["authenticated"] == "cookie"
# Cookie is usable with basic auth if usernames match
resp = Couch.get(
"/_session",
no_auth: true,
headers: [
authorization: "Basic #{:base64.encode("jan:apple")}",
cookie: token])
assert resp.status_code == 200
assert resp.body["userCtx"]["name"] == "jan"
assert resp.body["info"]["authenticated"] == "cookie"
# Cookie is not usable with basic auth if usernames don't match
resp = Couch.get(
"/_session",
no_auth: true,
headers: [
authorization: "Basic #{:base64.encode("notjan:banana")}",
cookie: token])
assert resp.status_code == 401
end
end