You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ch...@apache.org on 2018/06/22 22:16:00 UTC

[couchdb] 31/31: Port the first half of security_validation_tests.js

This is an automated email from the ASF dual-hosted git repository.

chewbranca pushed a commit to branch elixir-suite
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 656934a9be65ca678d3dfb56cc72cae35259b3a4
Author: Russell Branca <ch...@apache.org>
AuthorDate: Fri Jun 22 22:12:28 2018 +0000

    Port the first half of security_validation_tests.js
---
 test/elixir/lib/couch.ex                      |   8 +-
 test/elixir/test/security_validation_test.exs | 309 ++++++++++++++++++++++++++
 2 files changed, 316 insertions(+), 1 deletion(-)

diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex
index 3a491e8..6ae702e 100644
--- a/test/elixir/lib/couch.ex
+++ b/test/elixir/lib/couch.ex
@@ -67,8 +67,14 @@ defmodule Couch do
 
 
   def process_options(options) do
+
     if Keyword.get(options, :cookie) == nil do
-      Keyword.put(options, :basic_auth, {"adm", "pass"})
+      headers = Keyword.get(options, :headers, [])
+      if headers[:basic_auth] != nil or headers[:authorization] != nil do
+        options
+      else
+        Keyword.put(options, :basic_auth, {"adm", "pass"})
+      end
     else
       options
     end
diff --git a/test/elixir/test/security_validation_test.exs b/test/elixir/test/security_validation_test.exs
new file mode 100644
index 0000000..32117e2
--- /dev/null
+++ b/test/elixir/test/security_validation_test.exs
@@ -0,0 +1,309 @@
+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
+    resp = Couch.get("/_session")
+    Map.put(test_doc, "author", "tom")
+    resp = Couch.put("/#{db_name}/test_doc", body: test_doc)
+    assert resp.body["ok"]
+    Map.put(test_doc, "_rev", resp.body["rev"])
+
+    # Now Tom can update the document
+    Map.put(test_doc, "foo", "asdf")
+    resp = Couch.put("/#{db_name}/test_doc", [body: test_doc, headers: tom])
+    assert resp.body["ok"]
+    Map.put(test_doc, "_rev", resp.body["rev"])
+
+    # Jerry can't delete it
+    resp = Couch.delete("/#{db_name}/test_doc?rev=#{test_doc["_rev"]}", headers: jerry)
+    assert resp.status_code == 401
+    assert resp.body["error"] == "unauthorized"
+  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);
+#};