Skip to content
Open
2 changes: 2 additions & 0 deletions src/couch/src/couch_query_servers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
case Resp of
ok ->
ok;
{[{<<"forbidden">>, Message}, {<<"failures">>, Failures}]} ->
throw({forbidden, Message, Failures});
Copy link
Copy Markdown
Contributor

@nickva nickva Apr 29, 2026

Choose a reason for hiding this comment

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

We're changing the forbidden tuple shape. Make sure to check all the places which handle {forbidden, _} and now they may have to handle the triple-arg version. I noticed fabric_doc_update needs to handle this src/fabric/src/fabric_doc_update.erl

We should also call out and see if this will affect online cluster upgrades (new worker nodes throwing it and old coordinator nodes getting function clause errors). It maybe be fine, just needs an extra careful look at it.

{[{<<"forbidden">>, Message}]} ->
throw({forbidden, Message});
{[{<<"unauthorized">>, Message}]} ->
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.
]
}
}
32 changes: 24 additions & 8 deletions src/mango/src/mango_native_proc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,30 @@ 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 mango_selector:has_allowed_fields(Selector, [<<"newDoc">>, <<"oldDoc">>]) of
false ->
Msg =
<<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
Copy link
Copy Markdown
Contributor

@nickva nickva Apr 29, 2026

Choose a reason for hiding this comment

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

I wonder when this would fire, would it be on every time we attempt to insert a doc we'd crash the prompt?

{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
true ->
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
Struct =
case OldDoc of
null -> {[{<<"newDoc">>, NewDoc}]};
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.

Does this mean before we allowed OldDoc = null and now we skip it when it's null? I guess we haven't made a release so it won't break anything.

Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
end,
Reply =
case mango_selector:match_failures(Selector, Struct) of
[] ->
true;
Failures ->
{[
{<<"forbidden">>, <<"forbidden">>},
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 would change the message to <<"forbidden">> from <<"document is not valid">>. It does seems a bit redundant as the error is already forbidden

{<<"failures">>, Failures}
]}
end,
{reply, Reply, St}
end
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
Expand Down
Loading