diff options
author | Juanjo Rodriguez <juanjo@apache.org> | 2020-10-04 01:50:21 +0200 |
---|---|---|
committer | Juanjo Rodriguez <jjrodrig@gmail.com> | 2020-10-07 11:58:10 +0200 |
commit | f85e87916559b601b1287382645b517fbb7457bc (patch) | |
tree | ee88c3ab954b185ab70096ec79313283851fa359 /test | |
parent | 38f6ecad8f065e1e6049bf53362a84ed512e5b96 (diff) | |
download | couchdb-f85e87916559b601b1287382645b517fbb7457bc.tar.gz |
port users_db_security tests to elixir
Diffstat (limited to 'test')
-rw-r--r-- | test/elixir/README.md | 2 | ||||
-rw-r--r-- | test/elixir/lib/couch.ex | 23 | ||||
-rw-r--r-- | test/elixir/test/users_db_security_test.exs | 520 | ||||
-rw-r--r-- | 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 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"}); |