diff options
author | Paul J. Davis <paul.joseph.davis@gmail.com> | 2020-10-12 14:08:01 -0500 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2020-10-20 15:39:11 -0500 |
commit | bfda16348f5c61e274ad5087e4a6407fa8306e0d (patch) | |
tree | abda84f2b113ec97b61cec290b51f45d26e5e669 | |
parent | 79f71ec4810e13ae958f33e40c7c1d58d0a6cb99 (diff) | |
download | couchdb-bfda16348f5c61e274ad5087e4a6407fa8306e0d.tar.gz |
Allow for disabling custom reduce functions
This changes allows administators to disable custom JavaScript reduce
functions. Custom reduce functions are notoriously difficult to write
correctly and most of the time when written correctly would be more
efficiently done using an builtin reduce.
-rw-r--r-- | rel/overlay/etc/default.ini | 3 | ||||
-rw-r--r-- | src/chttpd/src/chttpd.erl | 2 | ||||
-rw-r--r-- | src/couch_views/src/couch_views_reader.erl | 10 | ||||
-rw-r--r-- | src/couch_views/src/couch_views_trees.erl | 2 | ||||
-rw-r--r-- | src/couch_views/src/couch_views_util.erl | 34 | ||||
-rw-r--r-- | src/couch_views/test/couch_views_custom_red_test.erl | 194 |
6 files changed, 240 insertions, 5 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 8970be572..ac4e2fa13 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -323,6 +323,9 @@ iterations = 10 ; iterations for password hashing ; Settings for view indexing [couch_views] +; Enable custom reduce functions +;custom_reduce_enabled = true + ; Maximum acceptors waiting to accept view indexing jobs ;max_acceptors = 5 ; diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl index 1a9b19bb1..3100694ff 100644 --- a/src/chttpd/src/chttpd.erl +++ b/src/chttpd/src/chttpd.erl @@ -1017,6 +1017,8 @@ error_info(all_workers_died) -> "request due to overloading or maintenance mode.">>}; error_info(not_implemented) -> {501, <<"not_implemented">>, <<"this feature is not yet implemented">>}; +error_info({disabled, Reason}) -> + {501, <<"disabled">>, Reason}; error_info(timeout) -> {500, <<"timeout">>, <<"The request could not be processed in a reasonable" " amount of time.">>}; diff --git a/src/couch_views/src/couch_views_reader.erl b/src/couch_views/src/couch_views_reader.erl index 3c5862749..35ee8a021 100644 --- a/src/couch_views/src/couch_views_reader.erl +++ b/src/couch_views/src/couch_views_reader.erl @@ -245,11 +245,19 @@ get_map_view(Lang, Args, ViewName, Views) -> get_red_view(Lang, Args, ViewName, Views) -> case couch_mrview_util:extract_view(Lang, Args, ViewName, Views) of - {red, {Idx, Lang, View}, _} -> {Idx, Lang, View}; + {red, {Idx, Lang, View}, _} -> check_red_enabled({Idx, Lang, View}); _ -> throw({not_found, missing_named_view}) end. +check_red_enabled({Idx, _Lang, View} = Resp) -> + case lists:nth(Idx, View#mrview.reduce_funs) of + {_, disabled} -> + throw({disabled, <<"Custom reduce functions are disabled.">>}); + _ -> + Resp + end. + expand_keys_args(#mrargs{keys = undefined} = Args) -> [Args]; diff --git a/src/couch_views/src/couch_views_trees.erl b/src/couch_views/src/couch_views_trees.erl index b45750be9..d9340ad30 100644 --- a/src/couch_views/src/couch_views_trees.erl +++ b/src/couch_views/src/couch_views_trees.erl @@ -323,7 +323,7 @@ make_read_only_reduce_fun(Lang, View, NthRed) -> make_reduce_fun(Lang, #mrview{} = View) -> - RedFuns = [Src || {_, Src} <- View#mrview.reduce_funs], + RedFuns = [Src || {_, Src} <- View#mrview.reduce_funs, Src /= disabled], fun (KVs0, _ReReduce = false) -> KVs1 = expand_dupes(KVs0), diff --git a/src/couch_views/src/couch_views_util.erl b/src/couch_views/src/couch_views_util.erl index 1e3e4beef..70400203c 100644 --- a/src/couch_views/src/couch_views_util.erl +++ b/src/couch_views/src/couch_views_util.erl @@ -65,7 +65,8 @@ ddoc_to_mrst(DbName, #doc{id=Id, body={Fields}}) -> NumViews = fun({_, View}, N) -> {View#mrview{id_num = N}, N+1} end, - {Views, _} = lists:mapfoldl(NumViews, 0, lists:sort(dict:to_list(BySrc))), + {Views0, _} = lists:mapfoldl(NumViews, 0, lists:sort(dict:to_list(BySrc))), + Views1 = maybe_disable_custom_reduce_funs(Views0), Language = couch_util:get_value(<<"language">>, Fields, <<"javascript">>), Lib = couch_util:get_value(<<"lib">>, RawViews, {[]}), @@ -74,12 +75,12 @@ ddoc_to_mrst(DbName, #doc{id=Id, body={Fields}}) -> db_name=DbName, idx_name=Id, lib=Lib, - views=Views, + views=Views1, language=Language, design_opts=DesignOpts, partitioned=Partitioned }, - SigInfo = {Views, Language, DesignOpts, couch_index_util:sort_lib(Lib)}, + SigInfo = {Views1, Language, DesignOpts, couch_index_util:sort_lib(Lib)}, {ok, IdxState#mrst{sig=couch_hash:md5_hash(term_to_binary(SigInfo))}}. @@ -327,6 +328,33 @@ active_tasks_info(ChangesDone, DbName, DDocId, LastSeq, DBSeq) -> }. +maybe_disable_custom_reduce_funs(Views) -> + case config:get_boolean("couch_views", "custom_reduce_enabled", true) of + true -> + Views; + false -> + disable_custom_reduce_funs(Views) + end. + + +disable_custom_reduce_funs(Views) -> + lists:map(fun(View) -> + #mrview{ + reduce_funs = ReduceFuns + } = View, + {Builtin, Custom} = lists:partition(fun({_Name, RedSrc}) -> + case RedSrc of + <<"_", _/binary>> -> true; + <<_/binary>> -> false + end + end, ReduceFuns), + DisabledCustom = [{Name, disabled} || {Name, _Src} <- Custom], + View#mrview{ + reduce_funs = Builtin ++ DisabledCustom + } + end, Views). + + convert_seq_to_stamp(<<"0">>) -> <<"0-0-0">>; diff --git a/src/couch_views/test/couch_views_custom_red_test.erl b/src/couch_views/test/couch_views_custom_red_test.erl new file mode 100644 index 000000000..911846d69 --- /dev/null +++ b/src/couch_views/test/couch_views_custom_red_test.erl @@ -0,0 +1,194 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_views_custom_red_test). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). +-include_lib("couch_mrview/include/couch_mrview.hrl"). +-include_lib("fabric/test/fabric2_test.hrl"). +-include("couch_views.hrl"). + + +-define(NUM_DOCS, 100). + + +custom_reduce_disabled_test_() -> + { + "Custom Reduce Disabled", + { + setup, + fun setup_disabled/0, + fun teardown/1, + with([ + ?TDEF(builtin_reductions_work), + ?TDEF(custom_reduces_disabled) + ]) + } + }. + + +custom_reduce_enabled_test_() -> + { + "Custom Reduce Disabled", + { + setup, + fun setup_enabled/0, + fun teardown/1, + with([ + ?TDEF(builtin_reductions_work), + ?TDEF(custom_reduces_enabled) + ]) + } + }. + + +sigs_change_test_() -> + { + "Sigs Change Test", + { + setup, + fun setup_sigs_change/0, + fun teardown_sigs_change/1, + with([ + ?TDEF(sigs_change) + ]) + } + }. + +setup_disabled() -> + setup_common(false). + + +setup_enabled() -> + setup_common(true). + + +setup_common(Enabled) -> + Ctx = test_util:start_couch([ + fabric, + couch_jobs, + couch_js, + couch_views + ]), + config:set_boolean("couch_views", "custom_reduce_enabled", Enabled, false), + {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]), + fabric2_db:update_docs(Db, [create_ddoc()]), + make_docs(Db, ?NUM_DOCS), + run_query(Db, <<"builtin">>, #{limit => 0}), + {Db, Ctx}. + + +teardown({Db, Ctx}) -> + fabric2_db:delete(fabric2_db:name(Db), [{user_ctx, ?ADMIN_USER}]), + test_util:stop_couch(Ctx). + + +setup_sigs_change() -> + meck:new(config, [passthrough]), + meck:expect(config, get, fun(_, _, Default) -> Default end). + + +teardown_sigs_change(_) -> + meck:unload(). + + +builtin_reductions_work({Db, _}) -> + Result = run_query(Db, <<"builtin">>, #{}), + Expect = {ok, [row(null, ?NUM_DOCS)]}, + ?assertEqual(Expect, Result). + + +custom_reduces_disabled({Db, _}) -> + ?assertThrow({disabled, _}, run_query(Db, <<"custom">>, #{})). + + +custom_reduces_enabled({Db, _}) -> + Result = run_query(Db, <<"custom">>, #{}), + Expect = {ok, [row(null, <<"silly_reduce">>)]}, + ?assertEqual(Expect, Result). + + +sigs_change(_) -> + meck:expect(config, get_boolean, fun("couch_views", _, _) -> false end), + {ok, Mrst1} = couch_views_util:ddoc_to_mrst(<<"foo">>, create_ddoc()), + meck:expect(config, get_boolean, fun("couch_views", _, _) -> true end), + {ok, Mrst2} = couch_views_util:ddoc_to_mrst(<<"foo">>, create_ddoc()), + ?assertNotEqual(Mrst1#mrst.sig, Mrst2#mrst.sig). + + +run_query(Db, Idx, Args) -> + DDoc = create_ddoc(), + run_query(Db, DDoc, Idx, Args). + + +run_query(Db, DDoc, Idx, Args) -> + couch_views:query(Db, DDoc, Idx, fun default_cb/2, [], Args). + + +default_cb(complete, Acc) -> + {ok, lists:reverse(Acc)}; +default_cb({final, Info}, []) -> + {ok, [Info]}; +default_cb({final, _}, Acc) -> + {ok, Acc}; +default_cb({meta, _}, Acc) -> + {ok, Acc}; +default_cb(ok, ddoc_updated) -> + {ok, ddoc_updated}; +default_cb(Row, Acc) -> + {ok, [Row | Acc]}. + + +row(Key, Value) -> + {row, [{key, Key}, {value, Value}]}. + + +create_ddoc() -> + couch_doc:from_json_obj({[ + {<<"_id">>, <<"_design/bar">>}, + {<<"views">>, {[ + {<<"custom">>, {[ + {<<"map">>, <<"function(doc) {emit(doc.val, doc.val);}">>}, + {<<"reduce">>, << + "function(keys, values, rereduce) {\n" + " return \"silly_reduce\";\n" + "}\n" + >>} + ]}}, + {<<"builtin">>, {[ + {<<"map">>, <<"function(doc) {emit(doc.val, doc.val);}">>}, + {<<"reduce">>, <<"_count">>} + ]}} + ]}} + ]}). + + +make_docs(Db, TotalDocs) when TotalDocs > 0 -> + make_docs(Db, TotalDocs, 0). + + +make_docs(Db, TotalDocs, DocsMade) when TotalDocs > DocsMade -> + DocCount = min(TotalDocs - DocsMade, 500), + Docs = [doc(I + DocsMade) || I <- lists:seq(1, DocCount)], + fabric2_db:update_docs(Db, Docs), + make_docs(Db, TotalDocs, DocsMade + DocCount); + +make_docs(_Db, TotalDocs, DocsMade) when TotalDocs =< DocsMade -> + ok. + + +doc(Id) -> + couch_doc:from_json_obj({[ + {<<"_id">>, list_to_binary(integer_to_list(Id))}, + {<<"val">>, Id} + ]}). |