diff options
Diffstat (limited to 'test/elixir/test/security_validation_test.exs')
-rw-r--r-- | test/elixir/test/security_validation_test.exs | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/test/elixir/test/security_validation_test.exs b/test/elixir/test/security_validation_test.exs new file mode 100644 index 000000000..526f06b2a --- /dev/null +++ b/test/elixir/test/security_validation_test.exs @@ -0,0 +1,310 @@ +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: [ + authorization: "Basic amVycnk6bW91c2U=" # jerry:mouse + ], + tom: [ + authorization: "Basic dG9tOmNhdA==" # tom:cat + ], + spike_cat: [ + authorization: "Basic c3Bpa2U6Y2F0" # spike:cat - which is wrong + ] + } + + @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 + headers = @auth_headers[:spike_cat] # spike:cat - which is wrong + 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 + headers = @auth_headers[:spike_cat] # spike:cat - which is wrong + 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); +#}; |