summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuanjo Rodriguez <juanjo@apache.org>2020-10-04 01:50:21 +0200
committerJuanjo Rodriguez <jjrodrig@gmail.com>2020-10-07 11:58:10 +0200
commitf85e87916559b601b1287382645b517fbb7457bc (patch)
treeee88c3ab954b185ab70096ec79313283851fa359
parent38f6ecad8f065e1e6049bf53362a84ed512e5b96 (diff)
downloadcouchdb-f85e87916559b601b1287382645b517fbb7457bc.tar.gz
port users_db_security tests to elixir
-rw-r--r--test/elixir/README.md2
-rw-r--r--test/elixir/lib/couch.ex23
-rw-r--r--test/elixir/test/users_db_security_test.exs520
-rw-r--r--test/javascript/tests/users_db_security.js2
4 files changed, 540 insertions, 7 deletions
diff --git a/test/elixir/README.md b/test/elixir/README.md
index de9642b16..51f83ef36 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 7819299cc..d9751c416 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 000000000..7b2c97df9
--- /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 faffd8c27..3e293c5eb 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"});