Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/couch/src/couch_query_servers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -477,18 +477,18 @@ builtin_cmp_last(A, B) ->
validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
JsonDiskDoc = json_doc(DiskDoc),
Resp = ddoc_prompt(
Db,
DDoc,
[<<"validate_doc_update">>],
[JsonEditDoc, JsonDiskDoc, Ctx, SecObj]
),
if
Resp == 1 -> ok;
true -> couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1)
end,
Args = [JsonEditDoc, JsonDiskDoc, Ctx, SecObj],

Resp =
case ddoc_prompt(Db, DDoc, [<<"validate_doc_update">>], Args) of
Code when Code =:= 1; Code =:= ok; Code =:= true ->
ok;
Other ->
couch_stats:increment_counter([couchdb, query_server, vdu_rejects], 1),
Other
end,
case Resp of
RespCode when RespCode =:= 1; RespCode =:= ok; RespCode =:= true ->
ok ->
ok;
{[{<<"forbidden">>, Message}]} ->
throw({forbidden, Message});
Expand Down
2 changes: 1 addition & 1 deletion src/couch_mrview/src/couch_mrview.erl
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ validate_ddoc_fields(DDoc) ->
[{<<"rewrites">>, [string, array]}],
[{<<"shows">>, object}, {any, [object, string]}],
[{<<"updates">>, object}, {any, [object, string]}],
[{<<"validate_doc_update">>, string}],
[{<<"validate_doc_update">>, [string, object]}],
[{<<"views">>, object}, {<<"lib">>, object}],
[{<<"views">>, object}, {any, object}, {<<"map">>, MapFuncType}],
[{<<"views">>, object}, {any, object}, {<<"reduce">>, string}]
Expand Down
27 changes: 27 additions & 0 deletions src/mango/src/mango_native_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

-record(st, {
indexes = [],
validators = [],
timeout = 5000
}).

Expand Down Expand Up @@ -94,6 +95,32 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
Else
end,
{reply, Vals, St};
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
NewSt =
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
undefined ->
St;
Selector0 ->
Selector = mango_selector:normalize(Selector0),
Validators = couch_util:set_value(DDocId, St#st.validators, Selector),
St#st{validators = Validators}
end,
{reply, true, NewSt};
handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _From, St) ->
case couch_util:get_value(DDocId, St#st.validators) of
undefined ->
Msg = [<<"validate_doc_update">>, DDocId],
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
Selector ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
Reply =
case mango_selector:match(Selector, Struct) of
true -> true;
_ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
end,
{reply, Reply, St}
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

Expand Down
12 changes: 12 additions & 0 deletions test/elixir/test/config/suite.elixir
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,18 @@
"serial execution is not spuriously counted as loop on test_rewrite_suite_db",
"serial execution is not spuriously counted as loop on test_rewrite_suite_db%2Fwith_slashes"
],
"ValidateDocUpdateTest": [
"JavaScript VDU accepts a valid document",
"JavaScript VDU rejects an invalid document",
"JavaScript VDU accepts a valid change",
"JavaScript VDU rejects an invalid change",
"Mango VDU accepts a valid document",
"Mango VDU rejects an invalid document",
"updating a Mango VDU updates its effects",
"converting a Mango VDU to JavaScript updates its effects",
"deleting a Mango VDU removes its effects",
"Mango VDU rejects a doc if any existing ddoc fails to match",
],
"SecurityValidationTest": [
"Author presence and user security",
"Author presence and user security when replicated",
Expand Down
212 changes: 212 additions & 0 deletions test/elixir/test/validate_doc_update_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
defmodule ValidateDocUpdateTest do
use CouchTestCase

@moduledoc """
Test validate_doc_update behaviour
"""

@js_type_check %{
language: "javascript",

validate_doc_update: ~s"""
function (newDoc) {
if (!newDoc.type) {
throw {forbidden: 'Documents must have a type field'};
}
}
"""
}

@tag :with_db
test "JavaScript VDU accepts a valid document", context do
db = context[:db_name]
Couch.put("/#{db}/_design/js-test", body: @js_type_check)

resp = Couch.put("/#{db}/doc", body: %{"type" => "movie"})
assert resp.status_code == 201
assert resp.body["ok"] == true
end

@tag :with_db
test "JavaScript VDU rejects an invalid document", context do
db = context[:db_name]
Couch.put("/#{db}/_design/js-test", body: @js_type_check)

resp = Couch.put("/#{db}/doc", body: %{"not" => "valid"})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end

@js_change_check %{
language: "javascript",

validate_doc_update: ~s"""
function (newDoc, oldDoc) {
if (oldDoc && newDoc.type !== oldDoc.type) {
throw {forbidden: 'Documents cannot change their type field'};
}
}
"""
}

@tag :with_db
test "JavaScript VDU accepts a valid change", context do
db = context[:db_name]
Couch.put("/#{db}/_design/js-test", body: @js_change_check)

Couch.put("/#{db}/doc", body: %{"type" => "movie"})

doc = Couch.get("/#{db}/doc").body
updated = doc |> Map.merge(%{"type" => "movie", "title" => "Duck Soup"})
resp = Couch.put("/#{db}/doc", body: updated)

assert resp.status_code == 201
end

@tag :with_db
test "JavaScript VDU rejects an invalid change", context do
db = context[:db_name]
Couch.put("/#{db}/_design/js-test", body: @js_change_check)

Couch.put("/#{db}/doc", body: %{"type" => "movie"})

doc = Couch.get("/#{db}/doc").body
updated = doc |> Map.put("type", "director")
resp = Couch.put("/#{db}/doc", body: updated)

assert resp.status_code == 403
end

@mango_type_check %{
language: "query",

validate_doc_update: %{
"newDoc" => %{"type" => %{"$exists" => true}}
}
}

@tag :with_db
test "Mango VDU accepts a valid document", context do
db = context[:db_name]
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc", body: %{"type" => "movie"})
assert resp.status_code == 201
assert resp.body["ok"] == true
end

@tag :with_db
test "Mango VDU rejects an invalid document", context do
db = context[:db_name]
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end

@tag :with_db
test "updating a Mango VDU updates its effects", context do
db = context[:db_name]

resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

ddoc = %{
language: "query",

validate_doc_update: %{
"newDoc" => %{
"type" => %{"$type" => "string"},
"year" => %{"$lt" => 2026}
}
}
}
resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]})
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994})
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc2", body: %{"type" => 42, "year" => 1994})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"

resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end

@tag :with_db
test "converting a Mango VDU to JavaScript updates its effects", context do
db = context[:db_name]

resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

ddoc = %{
language: "javascript",

validate_doc_update: ~s"""
function (newDoc) {
if (typeof newDoc.year !== 'number') {
throw {forbidden: 'Documents must have a valid year field'};
}
}
"""
}
resp = Couch.put("/#{db}/_design/mango-test", body: ddoc, query: %{rev: resp.body["rev"]})
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc1", body: %{"year" => 1994})
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc2", body: %{"year" => "1994"})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end

@tag :with_db
test "deleting a Mango VDU removes its effects", context do
db = context[:db_name]

resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

resp = Couch.delete("/#{db}/_design/mango-test", query: %{rev: resp.body["rev"]})
assert resp.status_code == 200

resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
assert resp.status_code == 201
end

@tag :with_db
test "Mango VDU rejects a doc if any existing ddoc fails to match", context do
db = context[:db_name]
resp = Couch.put("/#{db}/_design/mango-test", body: @mango_type_check)
assert resp.status_code == 201

ddoc = %{
language: "query",

validate_doc_update: %{
"newDoc" => %{"year" => %{"$lt" => 2026}}
}
}
resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc1", body: %{"type" => "movie", "year" => 1994})
assert resp.status_code == 201

resp = Couch.put("/#{db}/doc2", body: %{"year" => 1994})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"

resp = Couch.put("/#{db}/doc3", body: %{"type" => "movie", "year" => 2094})
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end
end