nickva commented on code in PR #5858:
URL: https://github.com/apache/couchdb/pull/5858#discussion_r3605668679


##########
src/mango/src/mango_native_proc.erl:
##########
@@ -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} ->

Review Comment:
   Could normalize throw other error besides `{mango_error, mango_selector, 
_}`? For example 
https://github.com/apache/couchdb/blob/2eed4886d1853b97459ffd4f0c827e0cdd816a91/src/mango/src/mango_selector.erl#L253
 (norm_fields) will throw `norm_fields`. Basically don't hardcode the module 
name, maybe something like `throw:{mango_error, _Mod, Error}` could work



##########
rel/overlay/etc/default.ini:
##########
@@ -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

Review Comment:
   We don't typically have live values in default.ini they should commented 
out. Since Mango VDUs are new (not released yet), we can probably always 
validate them. There is no reason to let uses shoot themselves in the foot. For 
JS, we'd keep it false since it's a compatibility issue. Maybe set up to be 
flipped in 4.x versions.



##########
src/couch_mrview/src/couch_mrview.erl:
##########
@@ -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

Review Comment:
   > Okay, I'll move these checks around so we avoid grabbing an OS process if 
it's not needed.
   
   I think we'd instead not want to call get_os_process() at all if we don't 
have views and won't be checking vdus (because we may have them but they are js 
vdus and we don't want to validate them because of the config)
   



##########
src/couch/src/couch_query_servers.erl:
##########
@@ -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]);

Review Comment:
   That makes sense as yeah. Wonder if we have any tests for it. Say if add a 
test for a vdu written in Lua. We shouldn't crash or try to validate it.



##########
src/couch/src/couch_query_servers.erl:
##########
@@ -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]);

Review Comment:
   One thing to check is that we may throw other errors besides 
`{compilation_error, _}` and `{os_process_error, _}`. In other words we'd 
ensure that we things like `invalid_operator` or something still either 
surfaces as a `{compilation_error, _}` or we handle the extra cases in the 
catch part.



##########
src/mango/src/mango_selector.erl:
##########
@@ -548,22 +679,136 @@ match({[{<<"$", _/binary>> = Op, _}]}, _, _) ->
 % We need to traverse value to find field. The call to
 % mango_doc:get_field/2 may return either not_found or
 % bad_path in which case matching fails.
-match({[{Field, Cond}]}, Value, Cmp) ->
+match({[{Field, Cond}]}, Value, #ctx{verbose = Verb, path = Path} = Ctx) ->
+    InnerPath = extend_path(Field, Path),
+    InnerCtx = Ctx#ctx{path = InnerPath},
     case mango_doc:get_field(Value, Field) of
         not_found when Cond == {[{<<"$exists">>, false}]} ->
-            true;
+            case Verb of
+                true -> [];
+                false -> true
+            end;
         not_found ->
-            false;
+            case Verb of
+                true -> [#failure{op = field, type = not_found, ctx = 
InnerCtx}];
+                false -> false
+            end;
         bad_path ->
-            false;
+            case Verb of
+                true -> [#failure{op = field, type = bad_path, ctx = 
InnerCtx}];
+                false -> false
+            end;
         SubValue when Field == <<"_id">> ->
-            match(Cond, SubValue, fun mango_json:cmp_raw/2);
+            match(Cond, SubValue, InnerCtx#ctx{cmp = fun 
mango_json:cmp_raw/2});
         SubValue ->
-            match(Cond, SubValue, Cmp)
+            match(Cond, SubValue, InnerCtx)
     end;
-match({[_, _ | _] = _Props} = Sel, _Value, _Cmp) ->
+match({[_, _ | _] = _Props} = Sel, _Value, _Ctx) ->
     error({unnormalized_selector, Sel}).
 
+extend_path(Field, Path) when is_binary(Field) ->
+    [Field | Path];
+extend_path(Field, Path) when is_list(Field) ->
+    lists:foldl(fun(F, Acc) -> [F | Acc] end, Path, Field).
+
+match_with_failure(Expr, Value, Op, Params, #ctx{negate = Neg} = Ctx) ->
+    case not match(Expr, Value, Ctx#ctx{verbose = false}) of
+        Neg -> [];
+        _ -> [#failure{op = Op, params = Params, ctx = Ctx}]
+    end.
+
+compare(_, _, #ctx{verbose = false}, Cond) ->
+    Cond;
+compare(Op, Arg, #ctx{negate = Neg} = Ctx, Cond) ->
+    case not Cond of
+        Neg -> [];
+        _ -> [#failure{op = Op, params = [Arg], ctx = Ctx}]
+    end.
+
+format_failure(#failure{op = Op, type = Type, params = Params, ctx = Ctx}) ->
+    Path = format_path(Ctx#ctx.path),
+    Msg = format_op(Op, Ctx#ctx.negate, Type, Params),
+    {[{<<"path">>, Path}, {<<"message">>, iolist_to_binary(Msg)}]}.
+
+format_op(Op, _, empty_list, _) ->
+    io_lib:format("operator $~p was invoked with an empty list", [Op]);
+format_op(Op, _, bad_value, [Value]) ->
+    io_lib:format("operator $~p was invoked with a bad value: ~s", [Op, 
jiffy:encode(Value)]);

Review Comment:
   I just meant that we can reuse the json_encode macro or the util function 
since it forces utf8 encoding and here we don't. It's mostly for consistency 
and belt and suspenders.



##########
test/elixir/test/validate_doc_update_test.exs:
##########
@@ -77,6 +77,36 @@ defmodule ValidateDocUpdateTest do
     assert resp.status_code == 403
   end
 
+  @tag :with_db
+  test "invalid JavaScript VDU is detected on doc update", context do
+    Couch.put("/_node/_local/_config/couchdb/validate_vdu", body: "\"false\"")

Review Comment:
   Look at may be using `set_config(...)` from `CouchTestCase`. For example I 
found this in one of the tests:
   
   ```
     @tag :with_partitioned_db
     test "partitioned query with query server config set", context do
       db_name = context[:db_name]
       create_partition_docs(db_name)
       create_index(db_name, ["value"])
   
       # this is to test that we bypass partition_query_limit for mango
       set_config({"query_server_config", "partition_query_limit", "1"})
   ```
   
   And then looking in `set_config`
   
   ```elixir
     def set_config({section, key, value}) do
       existing = set_config_raw(section, key, value)
   
       on_exit(fn ->
         Enum.each(existing, fn {node, prev_value} ->
           if prev_value != "" do
             url = "/_node/#{node}/_config/#{section}/#{key}"
             headers = ["X-Couch-Persist": "false"]
             body = :jiffy.encode(prev_value, [:use_nil])
             resp = Couch.put(url, headers: headers, body: body)
             assert resp.status_code == 200
           else
             url = "/_node/#{node}/_config/#{section}/#{key}"
             headers = ["X-Couch-Persist": "false"]
             resp = Couch.delete(url, headers: headers)
             assert resp.status_code == 200
           end
         end)
       end)
     end
   ```



##########
src/couch_mrview/src/couch_mrview.erl:
##########
@@ -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

Review Comment:
   I meant that we'd first see if we'd even need an os process and only then 
try to get it. I added a note about in a new review set of comments.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to