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
7 changes: 7 additions & 0 deletions rel/overlay/etc/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ view_index_dir = {{view_index_dir}}
; Javascript engine. The choices are: spidermonkey and quickjs
;js_engine = spidermonkey

; When set to "true", the `validate_doc_update` field will be validated when
; design documents are updated. For `javascript` design docs, the field must
; contain a well-formed JavaScript function, and for `query` design docs it
; must contain a Mango selector that is correctly structured to validate
; document updates.
validate_vdu = true

; Use cfile. This is a C-based file I/O module that can execute parallel file
; read calls. The regular Erlang VM file module, at least as of OTP 28 forces
; all file operations to go through a single controlling process which can
Expand Down
3 changes: 3 additions & 0 deletions share/server/dispatch-quickjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ globalThis.dispatch = function(line) {
case "reset":
State.reset.apply(null, cmd);
break;
case "validate_fun":
State.validateFun.apply(null, cmd);
break;
case "add_fun":
State.addFun.apply(null, cmd);
break;
Expand Down
1 change: 1 addition & 0 deletions share/server/loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ var Loop = function() {
"ddoc" : DDoc.ddoc,
// "view" : Views.handler,
"reset" : State.reset,
"validate_fun": State.validateFun,
"add_fun" : State.addFun,
"add_lib" : State.addLib,
"map_doc" : Views.mapDoc,
Expand Down
28 changes: 19 additions & 9 deletions share/server/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
// License for the specific language governing permissions and limitations under
// the License.

var makefun = function(newFun, option) {
switch (option) {
case 'nouveau':
var sandbox = create_nouveau_sandbox();
break;
default:
var sandbox = create_dreyfus_sandbox();
break;
}
return Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox);
}

var State = {
reset : function(config) {
// clear the globals and run gc
Expand All @@ -19,17 +31,15 @@ var State = {
gc();
print("true"); // indicates success
},
validateFun : function(newFun, option) {
// Validate a function but do not store it
makefun(newFun, option);
print("true");
},
addFun : function(newFun, option) {
// Compile to a function and add it to funs array
switch (option) {
case 'nouveau':
var sandbox = create_nouveau_sandbox();
break;
default:
var sandbox = create_dreyfus_sandbox();
break;
}
State.funs.push(Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox));
var fun = makefun(newFun, option);
State.funs.push(fun);
print("true");
},
addLib : function(lib) {
Expand Down
7 changes: 6 additions & 1 deletion src/couch/src/couch_query_servers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@

try_compile(Proc, FunctionType, FunctionName, FunctionSource) ->
try
proc_prompt(Proc, [<<"add_fun">>, FunctionSource]),
case FunctionType of
validate_doc_update ->
proc_prompt(Proc, [<<"validate_fun">>, FunctionSource]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if query language is Erlang? Do we get an immediate 500 or some nicer invalid language 400 response?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only validate the function if language is javascript or query; couch_mrview:validate() explicitly ends by accepting anything that's not recognised, i.e. you can save any design doc you like if it contains content CouchDB does not natively understand. This is very convenient for enabling other software to integrate with CouchDB.

We could add validation for erlang functions but that feels like expanding the scope of this PR into things that aren't strictly related and I'm not sure how to implement.

_ ->
proc_prompt(Proc, [<<"add_fun">>, FunctionSource])
end,
ok
catch
{compilation_error, E} ->
Expand Down
22 changes: 18 additions & 4 deletions src/couch_mrview/src/couch_mrview.erl
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,11 @@ validate(Db, DDoc) ->
ok
end,

try Views =/= [] andalso couch_query_servers:get_os_process(Lang) of
false ->
ok;
try couch_query_servers:get_os_process(Lang) of

@nickva nickva Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One change here to watch out for is that previously if ddoc didn't have views we didn't check out an OS process. Now we always do. Could we first check if we have views or a VDU and only then go into get_os_process()? Not a huge deal but OS processes are a limited resource and it would nice not have get them from the proc manager gen_server on every ddoc update if doesn't have anything to validate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll move these checks around so we avoid grabbing an OS process if it's not needed.

Proc ->
try
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views)
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views),
validate_vdu(Proc, DDoc)
after
couch_query_servers:ret_os_process(Proc)
end
Expand All @@ -263,6 +262,21 @@ validate(Db, DDoc) ->
ok
end.

validate_vdu(Proc, #doc{body = {Props}}) ->
case config:get_boolean("couchdb", "validate_vdu", false) of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be true as per default.ini value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the default.ini value is wrong, I've fixed that.

true ->
case couch_util:get_value(<<"validate_doc_update">>, Props) of
undefined ->
ok;
VDU ->
couch_query_servers:try_compile(
Proc, validate_doc_update, <<"validate_doc_update">>, VDU
)
end;
_ ->
ok
end.

check_rank(<<N/binary>>) ->
try binary_to_integer(N) of
Val when Val >= 1 andalso Val =< ?MAX_RANK ->
Expand Down
2 changes: 1 addition & 1 deletion src/couch_scanner/test/eunit/couch_scanner_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ setup() ->
#{from => <<"x">>, to => <<"y">>}
],
updates => #{u1 => <<"function(d,r){return [];}">>},
validate_doc_update => <<"function(n,o,u,s){return true;">>
validate_doc_update => <<"function(n,o,u,s){return true;}">>
}),
ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}),
ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => this_is_ok_apparently}),
Expand Down
13 changes: 13 additions & 0 deletions src/docs/src/config/couchdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ Base CouchDB Options
[couchdb]
js_engine = spidermonkey

.. config:option:: validate_vdu :: Enable checking of ``validate_doc_update``

.. versionadded:: TODO

When set to ``true``, the ``validate_doc_update`` field will be
validated when design documents are updated. For ``javascript`` design
docs, the field must contain a well-formed JavaScript function, and for
``query`` design docs it must contain a Mango selector that is correctly
structured to validate document updates. ::

[couchdb]
validate_vdu = true

.. config:option:: time_seq_min_time :: Minimum time-seq threshold

.. versionchanged:: 3.6
Expand Down
72 changes: 71 additions & 1 deletion src/docs/src/ddocs/ddocs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,8 @@ To use Mango selectors for validation, the design document must have the
containing the following fields:

* ``newDoc``: New version of document that will be stored.
* ``oldDoc``: Previous version of document that is already stored.
* ``oldDoc``: Previous version of document that is already stored; this field is
absent if the doc is being created for the first time.

For example, to check that all docs contain a ``title`` which is a string, and a
``year`` which is a number:
Expand Down Expand Up @@ -1012,3 +1013,72 @@ this design document:
}
}
}

By using the ``oldDoc`` field, we can create rules that say a document can only
be updated if it is currently in a certain state. For example, this rule would
enforce that only documents describing actors can be updated:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"oldDoc": { "type": "actor" }
}
}

This also makes it so that no new documents can be created, because a write is
only accepted if a previous version of the doc already exists. To relax this
constraint, allow ``oldDoc`` not to exist:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"oldDoc": {
"$or": [
{ "$exists": false },
{ "type": "actor" }
]
}
}
}

This validator will allow any new document creation, and updates to docs where
the ``type`` field is ``"actor"``. We can also have multiple rules for new
document states that depend on the current state, by combining ``$or`` with
several sets of ``{ oldDoc, newDoc }`` rules:

.. code-block:: json

{
"language": "query",

"validate_doc_update": {
"$or": [
// allow creation of docs with an acceptable type
{
"oldDoc": { "$exists": false },
"newDoc": {
"type": { "$in": ["movie", "actor"] }
}
},
// if a doc currently has "type": "actor", make sure its "movies"
// field is a non-empty list of strings
{
"oldDoc": { "type": "actor" },
"newDoc": {
"movies": {
"$type": "array",
"$not": { "$size": 0 },
"$allMatch": { "$type": "string" }
}
}
},
// etc.
]
}
}
55 changes: 46 additions & 9 deletions src/mango/src/mango_native_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut > 0 ->
gen_server:call(Pid, {set_timeout, TimeOut}).

prompt(Pid, Data) ->
gen_server:call(Pid, {prompt, Data}).
case gen_server:call(Pid, {prompt, Data}) of
{error, Error} ->
throw(Error);
Other ->
Other
end.

init(_) ->
{ok, #st{}}.
Expand Down Expand Up @@ -95,6 +100,17 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
Else
end,
{reply, Vals, St};
handle_call({prompt, [<<"validate_fun">>, Selector0 | _Rest]}, _From, St) ->
try mango_selector:normalize(Selector0) of
Selector ->
case validate_vdu(Selector) of
ok -> {reply, true, St};
Error -> {reply, {error, Error}, St}
end
catch
throw:{mango_error, mango_selector, Error} ->
{reply, {error, Error}, St}
end;
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
NewSt =
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
Expand All @@ -112,18 +128,39 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
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}
case validate_vdu(Selector) of
{_, Error} ->
{stop, {invalid_call, Error}, {invalid_call, Error}, St};
ok ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct =
case OldDoc of
null -> {[{<<"newDoc">>, NewDoc}]};
Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
end,
Reply =
case mango_selector:match_failures(Selector, Struct) of
[] ->
true;
Failures ->
{[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
end,
{reply, Reply, St}
end
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

validate_vdu(VDU) ->
case mango_selector:has_allowed_fields(VDU, [<<"newDoc">>, <<"oldDoc">>]) of
true ->
ok;
false ->
Msg =
<<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
{compilation_error, Msg}
end.

handle_cast(garbage_collect, St) ->
garbage_collect(),
{noreply, St};
Expand Down
Loading