blob: a020c507c6e6f59ae28c1164e2ca5b4aa1562910 [file] [log] [blame]
defmodule SecurityValidationTest do
use CouchTestCase
@moduletag :security
@moduledoc """
Test CouchDB Security Validations
This is a port of the security_validation.js suite
"""
@auth_headers %{
jerry: [
# jerry:mouse
authorization: "Basic amVycnk6bW91c2U="
],
tom: [
# tom:cat
authorization: "Basic dG9tOmNhdA=="
],
spike_cat: [
# spike:cat - which is wrong
authorization: "Basic c3Bpa2U6Y2F0"
]
}
@ddoc %{
_id: "_design/test",
language: "javascript",
validate_doc_update: ~s"""
(function (newDoc, oldDoc, userCtx, secObj) {
if (secObj.admin_override) {
if (userCtx.roles.indexOf('_admin') != -1) {
// user is admin, they can do anything
return true;
}
}
// docs should have an author field.
if (!newDoc._deleted && !newDoc.author) {
throw {forbidden:
\"Documents must have an author field\"};
}
if (oldDoc && oldDoc.author != userCtx.name) {
throw {unauthorized:
\"You are '\" + userCtx.name + \"', not the author '\" + oldDoc.author + \"' of this document. You jerk.\"};
}
})
"""
}
setup_all do
auth_db_name = random_db_name()
{:ok, _} = create_db(auth_db_name)
on_exit(fn -> delete_db(auth_db_name) end)
configs = [
{"httpd", "authentication_handlers",
"{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"},
{"couch_httpd_auth", "authentication_db", auth_db_name},
{"chttpd_auth", "authentication_db", auth_db_name}
]
Enum.each(configs, &set_config/1)
# port of comment from security_validation.js
# the special case handler does not exist (any longer) in clusters, so we have
# to replicate the behavior using a "normal" DB even though tests might no more
# run universally (why the "X-Couch-Test-Auth" header was introduced).
# btw: this needs to be INSIDE configured server to propagate correctly ;-)
# At least they'd run in the build, though
users = [{"tom", "cat"}, {"jerry", "mouse"}, {"spike", "dog"}]
Enum.each(users, fn {name, pass} ->
doc = %{
:_id => "org.couchdb.user:#{name}",
:name => name,
:roles => [],
:password => pass
}
assert Couch.post("/#{auth_db_name}", body: doc).body["ok"]
end)
{:ok, [auth_db_name: auth_db_name]}
end
@tag :with_db_name
test "Saving document using the wrong credentials", context do
# spike:cat - which is wrong
headers = @auth_headers[:spike_cat]
resp = Couch.post("/#{context[:db_name]}", body: %{foo: 1}, headers: headers)
assert resp.body["error"] == "unauthorized"
assert resp.status_code == 401
end
test "Force basic login" do
# spike:cat - which is wrong
headers = @auth_headers[:spike_cat]
resp = Couch.get("/_session", query: %{basic: true}, headers: headers)
assert resp.status_code == 401
assert resp.body["error"] == "unauthorized"
end
@tag :with_db
test "Jerry can save a document normally", context do
headers = @auth_headers[:jerry]
assert Couch.get("/_session", headers: headers).body["userCtx"]["name"] == "jerry"
doc = %{_id: "testdoc", foo: 1, author: "jerry"}
assert Couch.post("/#{context[:db_name]}", body: doc).body["ok"]
end
@tag :with_db
test "Non-admin user cannot save a ddoc", context do
headers = @auth_headers[:jerry]
resp = Couch.post("/#{context[:db_name]}", body: @ddoc, headers: headers)
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end
@tag :with_db
test "Ddoc writes with admin and replication contexts", context do
db_name = context[:db_name]
sec_obj = %{admins: %{names: ["jerry"]}}
assert Couch.put("/#{db_name}/_security", body: sec_obj).body["ok"]
assert Couch.post("/#{db_name}", body: @ddoc).body["ok"]
new_rev = "2-642e20f96624a0aae6025b4dba0c6fb2"
ddoc = Map.put(@ddoc, :_rev, new_rev) |> Map.put(:foo, "bar")
headers = @auth_headers[:tom]
# attempt to save doc in replication context, eg ?new_edits=false
resp = Couch.put("/#{db_name}/#{ddoc[:_id]}", body: ddoc, headers: headers, query: %{new_edits: false})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end
test "_session API" do
headers = @auth_headers[:jerry]
resp = Couch.get("/_session", headers: headers)
assert resp.body["userCtx"]["name"] == "jerry"
assert resp.body["userCtx"]["roles"] == []
end
@tag :with_db
test "Author presence and user security", context do
db_name = context[:db_name]
sec_obj = %{admin_override: false, admins: %{names: ["jerry"]}}
jerry = @auth_headers[:jerry]
tom = @auth_headers[:tom]
assert Couch.put("/#{db_name}/_security", body: sec_obj).body["ok"]
assert Couch.post("/#{db_name}", body: @ddoc).body["ok"]
resp = Couch.put("/#{db_name}/test_doc", body: %{foo: 1}, headers: jerry)
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
assert resp.body["reason"] == "Documents must have an author field"
# Jerry can write the document
assert Couch.put("/#{db_name}/test_doc", body: %{foo: 1, author: "jerry"}, headers: jerry).body["ok"]
test_doc = Couch.get("/#{db_name}/test_doc").body
# Tom cannot write the document
resp = Couch.post("/#{db_name}", body: %{foo: 1}, headers: tom)
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
# Enable admin override for changing author values
assert Couch.put("/#{db_name}/_security", body: %{sec_obj | admin_override: true}).body["ok"]
# Change owner to Tom
test_doc = Map.put(test_doc, "author", "tom")
resp = Couch.put("/#{db_name}/test_doc", body: test_doc)
assert resp.body["ok"]
test_doc = Map.put(test_doc, "_rev", resp.body["rev"])
# Now Tom can update the document
test_doc = Map.put(test_doc, "foo", "asdf")
resp = Couch.put("/#{db_name}/test_doc", body: test_doc, headers: tom)
assert resp.body["ok"]
test_doc = Map.put(test_doc, "_rev", resp.body["rev"])
# Jerry can't delete it
retry_until(fn ->
opts = [headers: jerry]
resp = Couch.delete("/#{db_name}/test_doc?rev=#{test_doc["_rev"]}", opts)
resp.status_code == 401 and resp.body["error"] == "unauthorized"
end)
end
end
# TODO: port remainder of security_validation.js suite
# remaining bits reproduced below:
#
# // try to do something lame
# try {
# db.setDbProperty("_security", ["foo"]);
# T(false && "can't do this");
# } catch(e) {}
#
# // go back to normal
# T(db.setDbProperty("_security", {admin_override : false}).ok);
#
# // Now delete document
# T(user2Db.deleteDoc(doc).ok);
#
# // now test bulk docs
# var docs = [{_id:"bahbah",author:"jerry",foo:"bar"},{_id:"fahfah",foo:"baz"}];
#
# // Create the docs
# var results = db.bulkSave(docs);
#
# T(results[0].rev)
# T(results[0].error == undefined)
# T(results[1].rev === undefined)
# T(results[1].error == "forbidden")
#
# T(db.open("bahbah"));
# T(db.open("fahfah") == null);
#
#
# // now all or nothing with a failure - no more available on cluster
# /* var docs = [{_id:"booboo",author:"Damien Katz",foo:"bar"},{_id:"foofoo",foo:"baz"}];
#
# // Create the docs
# var results = db.bulkSave(docs, {all_or_nothing:true});
#
# T(results.errors.length == 1);
# T(results.errors[0].error == "forbidden");
# T(db.open("booboo") == null);
# T(db.open("foofoo") == null);
# */
#
# // Now test replication
# var AuthHeaders = {"Authorization": "Basic c3Bpa2U6ZG9n"}; // spike
# adminDbA = new CouchDB("" + db_name + "_a", {"X-Couch-Full-Commit":"false"});
# adminDbB = new CouchDB("" + db_name + "_b", {"X-Couch-Full-Commit":"false"});
# var dbA = new CouchDB("" + db_name + "_a", AuthHeaders);
# var dbB = new CouchDB("" + db_name + "_b", AuthHeaders);
# // looping does not really add value as the scenario is the same anyway (there's nothing 2 be gained from it)
# var A = CouchDB.protocol + CouchDB.host + "/" + db_name + "_a";
# var B = CouchDB.protocol + CouchDB.host + "/" + db_name + "_b";
#
# // (the databases never exist b4 - and we made sure they're deleted below)
# //adminDbA.deleteDb();
# adminDbA.createDb();
# //adminDbB.deleteDb();
# adminDbB.createDb();
#
# // save and replicate a documents that will and will not pass our design
# // doc validation function.
# T(dbA.save({_id:"foo1",value:"a",author:"tom"}).ok);
# T(dbA.save({_id:"foo2",value:"a",author:"spike"}).ok);
# T(dbA.save({_id:"bad1",value:"a"}).ok);
#
# T(CouchDB.replicate(A, B, {headers:AuthHeaders}).ok);
# T(CouchDB.replicate(B, A, {headers:AuthHeaders}).ok);
#
# T(dbA.open("foo1"));
# T(dbB.open("foo1"));
# T(dbA.open("foo2"));
# T(dbB.open("foo2"));
#
# // save the design doc to dbA
# delete designDoc._rev; // clear rev from previous saves
# T(adminDbA.save(designDoc).ok);
#
# // no affect on already saved docs
# T(dbA.open("bad1"));
#
# // Update some docs on dbB. Since the design hasn't replicated, anything
# // is allowed.
#
# // this edit will fail validation on replication to dbA (no author)
# T(dbB.save({_id:"bad2",value:"a"}).ok);
#
# // this edit will fail security on replication to dbA (wrong author
# // replicating the change)
# var foo1 = dbB.open("foo1");
# foo1.value = "b";
# T(dbB.save(foo1).ok);
#
# // this is a legal edit
# var foo2 = dbB.open("foo2");
# foo2.value = "b";
# T(dbB.save(foo2).ok);
#
# var results = CouchDB.replicate({"url": B, "headers": AuthHeaders}, {"url": A, "headers": AuthHeaders}, {headers:AuthHeaders});
# T(results.ok);
# TEquals(1, results.history[0].docs_written);
# TEquals(2, results.history[0].doc_write_failures);
#
# // bad2 should not be on dbA
# T(dbA.open("bad2") == null);
#
# // The edit to foo1 should not have replicated.
# T(dbA.open("foo1").value == "a");
#
# // The edit to foo2 should have replicated.
# T(dbA.open("foo2").value == "b");
# });
#
# // cleanup
# db.deleteDb();
# if(adminDbA){
# adminDbA.deleteDb();
# }
# if(adminDbB){
# adminDbB.deleteDb();
# }
# authDb.deleteDb();
# // have to clean up authDb on the backside :(
# var req = CouchDB.newXhr();
# req.open("DELETE", "http://127.0.0.1:15986/" + authDb_name, false);
# req.send("");
# CouchDB.maybeThrowError(req);
# };