You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ju...@apache.org on 2020/10/07 09:58:25 UTC

[couchdb] branch 3.x updated (38f6eca -> 8da8306)

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

juanjo pushed a change to branch 3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git.


    from 38f6eca  simplify max_document_size comment
     new f85e879  port users_db_security tests to elixir
     new 8da8306  Remove javascript tests from main build process

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Makefile                                    |   1 -
 Makefile.win                                |   1 -
 test/elixir/README.md                       |   2 +-
 test/elixir/lib/couch.ex                    |  23 +-
 test/elixir/test/users_db_security_test.exs | 520 ++++++++++++++++++++++++++++
 test/javascript/tests/users_db_security.js  |   2 +-
 6 files changed, 540 insertions(+), 9 deletions(-)
 create mode 100644 test/elixir/test/users_db_security_test.exs


[couchdb] 02/02: Remove javascript tests from main build process

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

juanjo pushed a commit to branch 3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 8da8306b5f160c4249cf700d327f547bd9c9bcd5
Author: Juanjo Rodriguez <ju...@apache.org>
AuthorDate: Mon Oct 5 08:02:10 2020 +0200

    Remove javascript tests from main build process
---
 Makefile     | 1 -
 Makefile.win | 1 -
 2 files changed, 2 deletions(-)

diff --git a/Makefile b/Makefile
index 564c908..f3d8e7a 100644
--- a/Makefile
+++ b/Makefile
@@ -148,7 +148,6 @@ fauxton: share/www
 # target: check - Test everything
 check: all python-black
 	@$(MAKE) eunit
-	@$(MAKE) javascript
 	@$(MAKE) mango-test
 	@$(MAKE) elixir
 
diff --git a/Makefile.win b/Makefile.win
index 36968f3..dbef9da 100644
--- a/Makefile.win
+++ b/Makefile.win
@@ -135,7 +135,6 @@ fauxton: share\www
 # target: check - Test everything
 check: all python-black
 	@$(MAKE) eunit
-	@$(MAKE) javascript
 	@$(MAKE) mango-test
 	@$(MAKE) elixir
 


[couchdb] 01/02: port users_db_security tests to elixir

Posted by ju...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

juanjo pushed a commit to branch 3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit f85e87916559b601b1287382645b517fbb7457bc
Author: Juanjo Rodriguez <ju...@apache.org>
AuthorDate: Sun Oct 4 01:50:21 2020 +0200

    port users_db_security tests to elixir
---
 test/elixir/README.md                       |   2 +-
 test/elixir/lib/couch.ex                    |  23 +-
 test/elixir/test/users_db_security_test.exs | 520 ++++++++++++++++++++++++++++
 test/javascript/tests/users_db_security.js  |   2 +-
 4 files changed, 540 insertions(+), 7 deletions(-)

diff --git a/test/elixir/README.md b/test/elixir/README.md
index de9642b..51f83ef 100644
--- a/test/elixir/README.md
+++ b/test/elixir/README.md
@@ -95,7 +95,7 @@ X means done, - means partially
   - [ ] Port stats.js
   - [X] Port update_documents.js
   - [X] Port users_db.js
-  - [ ] Port users_db_security.js
+  - [X] Port users_db_security.js
   - [X] Port utf8.js
   - [X] Port uuids.js
   - [X] Port view_collation.js
diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex
index 7819299..d9751c4 100644
--- a/test/elixir/lib/couch.ex
+++ b/test/elixir/lib/couch.ex
@@ -40,15 +40,28 @@ defmodule Couch.Session do
 
   # Skipping head/patch/options for YAGNI. Feel free to add
   # if the need arises.
-
   def go(%Couch.Session{} = sess, method, url, opts) do
-    opts = Keyword.merge(opts, cookie: sess.cookie)
-    Couch.request(method, url, opts)
+    parse_response = Keyword.get(opts, :parse_response, true)
+    opts = opts
+           |> Keyword.merge(cookie: sess.cookie)
+           |> Keyword.delete(:parse_response)
+    if parse_response do
+      Couch.request(method, url, opts)
+    else
+      Rawresp.request(method, url, opts)
+    end
   end
 
   def go!(%Couch.Session{} = sess, method, url, opts) do
-    opts = Keyword.merge(opts, cookie: sess.cookie)
-    Couch.request!(method, url, opts)
+    parse_response = Keyword.get(opts, :parse_response, true)
+    opts = opts
+           |> Keyword.merge(cookie: sess.cookie)
+           |> Keyword.delete(:parse_response)
+    if parse_response do
+      Couch.request!(method, url, opts)
+    else
+      Rawresp.request!(method, url, opts)
+    end
   end
 end
 
diff --git a/test/elixir/test/users_db_security_test.exs b/test/elixir/test/users_db_security_test.exs
new file mode 100644
index 0000000..7b2c97d
--- /dev/null
+++ b/test/elixir/test/users_db_security_test.exs
@@ -0,0 +1,520 @@
+defmodule UsersDbSecurityTest do
+  use CouchTestCase
+
+  @moduletag :authentication
+  @moduletag kind: :single_node
+
+  @users_db "_users"
+
+  @login_user %{
+    jerry: "apple",
+    tom: "mp3",
+    spike: "foobar",
+    speedy: "test",
+    silvestre: "anchovy"
+  }
+
+  setup_all do
+    # Create db if not exists
+    Couch.put("/#{@users_db}")
+
+    retry_until(fn ->
+      resp =
+        Couch.get(
+          "/#{@users_db}/_changes",
+          query: [feed: "longpoll", timeout: 5000, filter: "_design"]
+        )
+
+      length(resp.body["results"]) > 0
+    end)
+
+    on_exit(&tear_down/0)
+
+    :ok
+  end
+
+  defp tear_down do
+    users = Map.keys(@login_user)
+    Enum.each(users, fn name ->
+      resp = Couch.get("/#{@users_db}/org.couchdb.user:#{name}")
+      if resp.status_code == 200 do
+        rev = resp.body["_rev"]
+        Couch.delete("/#{@users_db}/org.couchdb.user:#{name}?rev=#{rev}")
+      end
+    end)
+  end
+
+  defp login_as(user, password \\ nil) do
+    pwd =
+      case password do
+        nil -> @login_user[String.to_atom(user)]
+        _ -> password
+      end
+
+    sess = Couch.login(user, pwd)
+    assert sess.cookie, "Login correct is expected"
+    sess
+  end
+
+  defp logout(session) do
+    assert Couch.Session.logout(session).body["ok"]
+  end
+
+  defp open_as(db_name, doc_id, options) do
+    use_session = Keyword.get(options, :use_session)
+    user = Keyword.get(options, :user)
+    pwd = Keyword.get(options, :pwd)
+    expect_response = Keyword.get(options, :expect_response, 200)
+    expect_message = Keyword.get(options, :error_message)
+
+    session = use_session || login_as(user, pwd)
+
+    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 view_as(db_name, view_name, options) do
+    use_session = Keyword.get(options, :use_session)
+    user = Keyword.get(options, :user)
+    pwd = Keyword.get(options, :pwd)
+    expect_response = Keyword.get(options, :expect_response, 200)
+    expect_message = Keyword.get(options, :error_message)
+
+    session = use_session || login_as(user, pwd)
+
+    [view_root, view_name] = String.split(view_name, "/")
+
+    resp =
+      Couch.Session.get(session, "/#{db_name}/_design/#{view_root}/_view/#{view_name}")
+
+    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 changes_as(db_name, 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.get(
+        session,
+        "/#{db_name}/_changes"
+      )
+
+    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 request_raw_as(db_name, path, options) do
+    use_session = Keyword.get(options, :use_session)
+    user = Keyword.get(options, :user)
+    pwd = Keyword.get(options, :pwd)
+    expect_response = Keyword.get(options, :expect_response, 200)
+    expect_message = Keyword.get(options, :error_message)
+
+    session = use_session || login_as(user, pwd)
+
+    resp =
+      Couch.Session.get(
+        session,
+        "/#{db_name}/#{path}",
+        parse_response: false
+      )
+
+    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 request_as(db_name, path, options) do
+    use_session = Keyword.get(options, :use_session)
+    user = Keyword.get(options, :user)
+    pwd = Keyword.get(options, :pwd)
+    expect_response = Keyword.get(options, :expect_response, 200)
+    expect_message = Keyword.get(options, :error_message)
+
+    session = use_session || login_as(user, pwd)
+
+    resp =
+      Couch.Session.get(
+        session,
+        "/#{db_name}/#{path}"
+      )
+
+    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 set_security(db_name, security, expect_response \\ 200) do
+    resp = Couch.put("/#{db_name}/_security", body: security)
+    assert resp.status_code == expect_response
+  end
+
+  @tag config: [
+         {
+           "couchdb",
+           "users_db_security_editable",
+           "true"
+         },
+         {
+           "couch_httpd_auth",
+           "iterations",
+           "1"
+         },
+         {
+           "admins",
+           "jerry",
+           "apple"
+         }
+       ]
+  test "user db security" do
+    # _users db
+    # a doc with a field 'password' should be hashed to 'derived_key'
+    # with salt and salt stored in 'salt', 'password' is set to null.
+    # Exising 'derived_key' and 'salt' fields are overwritten with new values
+    # when a non-null 'password' field exists.
+    # anonymous should be able to create a user document
+    user_doc = %{
+      _id: "org.couchdb.user:tom",
+      type: "user",
+      name: "tom",
+      password: "mp3",
+      roles: []
+    }
+
+    resp =
+      Couch.post("/#{@users_db}", body: user_doc, headers: [authorization: "annonymous"])
+
+    assert resp.status_code in [201, 202]
+    assert resp.body["ok"]
+
+    user_doc =
+      retry_until(fn ->
+        user_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
+        assert !user_doc["password"]
+        assert String.length(user_doc["derived_key"]) == 40
+        assert String.length(user_doc["salt"]) == 32
+        user_doc
+      end)
+
+    # anonymous should not be able to read an existing user's user document
+    resp =
+      Couch.get("/#{@users_db}/org.couchdb.user:tom",
+        headers: [authorization: "annonymous"]
+      )
+
+    assert resp.status_code == 404
+
+    # anonymous should not be able to read /_users/_changes
+    resp = Couch.get("/#{@users_db}/_changes", headers: [authorization: "annonymous"])
+    assert resp.status_code == 401
+    assert resp.body["error"] == "unauthorized"
+
+    # user should be able to read their own document
+    tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom")
+    assert tom_doc["_id"] == "org.couchdb.user:tom"
+
+    # user should not be able to read /_users/_changes
+    changes_as(@users_db,
+      user: "tom",
+      expect_response: 401,
+      expect_message: "unauthorized"
+    )
+
+    tom_doc = Map.put(tom_doc, "password", "couch")
+    save_as(@users_db, tom_doc, user: "tom")
+
+    tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry")
+    assert !tom_doc["password"]
+    assert String.length(tom_doc["derived_key"]) == 40
+    assert String.length(tom_doc["salt"]) == 32
+    assert tom_doc["derived_key"] != user_doc["derived_key"]
+    assert tom_doc["salt"] != user_doc["salt"]
+
+    # user should not be able to read another user's user document
+    spike_doc = %{
+      _id: "org.couchdb.user:spike",
+      type: "user",
+      name: "spike",
+      password: "foobar",
+      roles: []
+    }
+
+    {:ok, _} = create_doc(@users_db, spike_doc)
+
+    open_as(@users_db, "org.couchdb.user:spike",
+      user: "tom",
+      pwd: "couch",
+      expect_response: 404
+    )
+
+    speedy_doc = %{
+      _id: "org.couchdb.user:speedy",
+      type: "user",
+      name: "speedy",
+      password: "test",
+      roles: ["user_admin"]
+    }
+
+    {:ok, _} = create_doc(@users_db, speedy_doc)
+
+    security = %{
+      admins: %{
+        roles: [],
+        names: ["speedy"]
+      }
+    }
+
+    set_security(@users_db, security)
+
+    # user should not be able to read from any view
+    ddoc = %{
+      _id: "_design/user_db_auth",
+      views: %{
+        test: %{
+          map: "function(doc) { emit(doc._id, null); }"
+        }
+      },
+      lists: %{
+        names: """
+        function(head, req) {
+        var row; while (row = getRow()) { send(row.key + \"\\n\"); }
+        }
+        """
+      },
+      shows: %{
+        name: "function(doc, req) { return doc.name; }"
+      }
+    }
+
+    create_doc(@users_db, ddoc)
+
+    resp =
+      Couch.get("/#{@users_db}/_design/user_db_auth/_view/test",
+        headers: [authorization: "annonymous"]
+      )
+
+    assert resp.body["error"] == "forbidden"
+
+    # admin should be able to read from any view
+    resp = view_as(@users_db, "user_db_auth/test", user: "jerry")
+    assert resp.body["total_rows"] == 3
+
+    # db admin should be able to read from any view
+    resp = view_as(@users_db, "user_db_auth/test", user: "speedy")
+    assert resp.body["total_rows"] == 3
+
+    # non-admins can't read design docs
+    open_as(@users_db, "_design/user_db_auth",
+      user: "tom",
+      pwd: "couch",
+      expect_response: 403,
+      expect_message: "forbidden"
+    )
+
+    # admin shold be able to read _list
+    result =
+      request_raw_as(@users_db, "_design/user_db_auth/_list/names/test", user: "jerry")
+
+    assert result.status_code == 200
+    assert length(String.split(result.body, "\n")) == 4
+
+    # non-admins can't read _list
+    request_raw_as(@users_db, "_design/user_db_auth/_list/names/test",
+      user: "tom",
+      pwd: "couch",
+      expect_response: 403
+    )
+
+    # admin should be able to read _show
+    result =
+      request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:tom",
+        user: "jerry"
+      )
+
+    assert result.status_code == 200
+    assert result.body == "tom"
+
+    # non-admin should be able to access own _show
+    result =
+      request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:tom",
+        user: "tom",
+        pwd: "couch"
+      )
+
+    assert result.status_code == 200
+    assert result.body == "tom"
+
+    # non-admin can't read other's _show
+    request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:jerry",
+      user: "tom",
+      pwd: "couch",
+      expect_response: 404
+    )
+
+    # admin should be able to read and edit any user doc
+    spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry")
+    spike_doc = Map.put(spike_doc, "password", "mobile")
+    save_as(@users_db, spike_doc, user: "jerry")
+
+    # admin should be able to read and edit any user doc
+    spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry")
+    spike_doc = Map.put(spike_doc, "password", "mobile1")
+    save_as(@users_db, spike_doc, user: "speedy")
+
+    security = %{
+      admins: %{
+        roles: ["user_admin"],
+        names: []
+      }
+    }
+
+    set_security(@users_db, security)
+
+    # db admin should be able to read and edit any user doc
+    spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry")
+    spike_doc = Map.put(spike_doc, "password", "mobile2")
+    save_as(@users_db, spike_doc, user: "speedy")
+
+    # ensure creation of old-style docs still works
+    silvestre_doc = prepare_user_doc(name: "silvestre", password: "anchovy")
+
+    resp =
+      Couch.post("/#{@users_db}",
+        body: silvestre_doc,
+        headers: [authorization: "annonymous"]
+      )
+
+    assert resp.body["ok"]
+
+    run_on_modified_server(
+      [
+        %{
+          :section => "couch_httpd_auth",
+          :key => "public_fields",
+          :value => "name"
+        },
+        %{
+          :section => "couch_httpd_auth",
+          :key => "users_db_public",
+          :value => "false"
+        }
+      ],
+      fn ->
+        request_as(@users_db, "_all_docs?include_docs=true",
+          user: "tom",
+          pwd: "couch",
+          expect_response: 401,
+          expect_message: "unauthorized"
+        )
+
+        # COUCHDB-1888 make sure admins always get all fields
+        resp = request_as(@users_db, "_all_docs?include_docs=true", user: "jerry")
+        rows = resp.body["rows"]
+        assert Enum.at(rows, 2)["doc"]["type"] == "user"
+      end
+    )
+  end
+end
diff --git a/test/javascript/tests/users_db_security.js b/test/javascript/tests/users_db_security.js
index faffd8c..3e293c5 100644
--- a/test/javascript/tests/users_db_security.js
+++ b/test/javascript/tests/users_db_security.js
@@ -9,7 +9,7 @@
 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 // License for the specific language governing permissions and limitations under
 // the License.
-
+couchTests.elixir = true;
 couchTests.users_db_security = function(debug) {
   var db_name = '_users';
   var usersDb = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"});