summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Lehnardt <jan@apache.org>2022-06-25 11:28:53 +0200
committerJan Lehnardt <jan@apache.org>2022-12-16 16:56:18 +0100
commit9d3fe316d68da55967ade592205f49e9d0a89735 (patch)
treeabf850c2dfcfa8ce0e42b15405a65b238d65523e
parent8c3e295ce9c03304250308822834309320b00c97 (diff)
downloadcouchdb-9d3fe316d68da55967ade592205f49e9d0a89735.tar.gz
feat(access): add mrview machinery
-rw-r--r--src/couch_index/src/couch_index_updater.erl35
-rw-r--r--src/couch_mrview/include/couch_mrview.hrl3
-rw-r--r--src/couch_mrview/src/couch_mrview.erl112
-rw-r--r--src/couch_mrview/src/couch_mrview_updater.erl46
-rw-r--r--src/couch_mrview/src/couch_mrview_util.erl9
5 files changed, 186 insertions, 19 deletions
diff --git a/src/couch_index/src/couch_index_updater.erl b/src/couch_index/src/couch_index_updater.erl
index fe2150505..66d760622 100644
--- a/src/couch_index/src/couch_index_updater.erl
+++ b/src/couch_index/src/couch_index_updater.erl
@@ -123,8 +123,8 @@ update(Idx, Mod, IdxState) ->
IncludeDesign = lists:member(include_design, UpdateOpts),
DocOpts =
case lists:member(local_seq, UpdateOpts) of
- true -> [conflicts, deleted_conflicts, local_seq];
- _ -> [conflicts, deleted_conflicts]
+ true -> [conflicts, deleted_conflicts, local_seq, deleted];
+ _ -> [conflicts, deleted_conflicts,local_seq, deleted]
end,
couch_util:with_db(DbName, fun(Db) ->
@@ -142,23 +142,36 @@ update(Idx, Mod, IdxState) ->
end,
GetInfo = fun
- (#full_doc_info{id = Id, update_seq = Seq, deleted = Del} = FDI) ->
- {Id, Seq, Del, couch_doc:to_doc_info(FDI)};
- (#doc_info{id = Id, high_seq = Seq, revs = [RI | _]} = DI) ->
- {Id, Seq, RI#rev_info.deleted, DI}
+ (#full_doc_info{id=Id, update_seq=Seq, deleted=Del,access=Access}=FDI) ->
+ {Id, Seq, Del, couch_doc:to_doc_info(FDI), Access};
+ (#doc_info{id=Id, high_seq=Seq, revs=[RI|_],access=Access}=DI) ->
+ {Id, Seq, RI#rev_info.deleted, DI, Access}
end,
LoadDoc = fun(DI) ->
- {DocId, Seq, Deleted, DocInfo} = GetInfo(DI),
+ {DocId, Seq, Deleted, DocInfo, Access} = GetInfo(DI),
case {IncludeDesign, DocId} of
{false, <<"_design/", _/binary>>} ->
{nil, Seq};
- _ when Deleted ->
- {#doc{id = DocId, deleted = true}, Seq};
_ ->
- {ok, Doc} = couch_db:open_doc_int(Db, DocInfo, DocOpts),
- {Doc, Seq}
+ case IndexName of % TODO: move into outer case statement
+ <<"_design/_access">> ->
+ {ok, Doc} = couch_db:open_doc_int(Db, DocInfo, DocOpts),
+ % TODO: hande conflicted docs in _access index
+ % probably remove
+ [RevInfo|_] = DocInfo#doc_info.revs,
+ Doc1 = Doc#doc{
+ meta = [{body_sp, RevInfo#rev_info.body_sp}],
+ access = Access
+ },
+ {Doc1, Seq};
+ _ when Deleted ->
+ {#doc{id=DocId, deleted=true}, Seq};
+ _ ->
+ {ok, Doc} = couch_db:open_doc_int(Db, DocInfo, DocOpts),
+ {Doc, Seq}
+ end
end
end,
diff --git a/src/couch_mrview/include/couch_mrview.hrl b/src/couch_mrview/include/couch_mrview.hrl
index b31463c53..ef987595d 100644
--- a/src/couch_mrview/include/couch_mrview.hrl
+++ b/src/couch_mrview/include/couch_mrview.hrl
@@ -83,7 +83,8 @@
conflicts,
callback,
sorted = true,
- extra = []
+ extra = [],
+ deleted = false
}).
-record(vacc, {
diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl
index d8640c903..79b2b8bec 100644
--- a/src/couch_mrview/src/couch_mrview.erl
+++ b/src/couch_mrview/src/couch_mrview.erl
@@ -13,7 +13,7 @@
-module(couch_mrview).
-export([validate/2]).
--export([query_all_docs/2, query_all_docs/4]).
+-export([query_all_docs/2, query_all_docs/4, query_changes_access/5]).
-export([query_view/3, query_view/4, query_view/6, get_view_index_pid/4]).
-export([get_info/2]).
-export([trigger_update/2, trigger_update/3]).
@@ -259,6 +259,116 @@ query_all_docs(Db, Args) ->
query_all_docs(Db, Args, Callback, Acc) when is_list(Args) ->
query_all_docs(Db, to_mrargs(Args), Callback, Acc);
query_all_docs(Db, Args0, Callback, Acc) ->
+ case couch_db:has_access_enabled(Db) and not couch_db:is_admin(Db) of
+ true -> query_all_docs_access(Db, Args0, Callback, Acc);
+ false -> query_all_docs_admin(Db, Args0, Callback, Acc)
+ end.
+access_ddoc() ->
+ #doc{
+ id = <<"_design/_access">>,
+ body = {[
+ {<<"language">>,<<"_access">>},
+ {<<"options">>, {[
+ {<<"include_design">>, true}
+ ]}},
+ {<<"views">>, {[
+ {<<"_access_by_id">>, {[
+ {<<"map">>, <<"_access/by-id-map">>},
+ {<<"reduce">>, <<"_count">>}
+ ]}},
+ {<<"_access_by_seq">>, {[
+ {<<"map">>, <<"_access/by-seq-map">>},
+ {<<"reduce">>, <<"_count">>}
+ ]}}
+ ]}}
+ ]}
+ }.
+query_changes_access(Db, StartSeq, Fun, Options, Acc) ->
+ DDoc = access_ddoc(),
+ UserCtx = couch_db:get_user_ctx(Db),
+ UserName = UserCtx#user_ctx.name,
+ %% % TODO: add roles
+ Args1 = prefix_startkey_endkey(UserName, #mrargs{}, fwd),
+ Args2 = Args1#mrargs{deleted=true},
+ Args = Args2#mrargs{reduce=false},
+ %% % filter out the user-prefix from the key, so _all_docs looks normal
+ %% % this isn’t a separate function because I’m binding Callback0 and I don’t
+ %% % know the Erlang equivalent of JS’s fun.bind(this, newarg)
+ Callback = fun
+ ({meta, _}, Acc0) ->
+ {ok, Acc0}; % ignore for now
+ ({row, Props}, Acc0) ->
+ % turn row into FDI
+ Value = couch_util:get_value(value, Props),
+ [Owner, Seq] = couch_util:get_value(key, Props),
+ Rev = couch_util:get_value(rev, Value),
+ Deleted = couch_util:get_value(deleted, Value, false),
+ BodySp = couch_util:get_value(body_sp, Value),
+ [Pos, RevId] = string:split(?b2l(Rev), "-"),
+ FDI = #full_doc_info{
+ id = proplists:get_value(id, Props),
+ rev_tree = [{list_to_integer(Pos), {?l2b(RevId), #leaf{deleted=Deleted, ptr=BodySp, seq=Seq, sizes=#size_info{}}, []}}],
+ deleted = Deleted,
+ update_seq = 0,
+ sizes = #size_info{},
+ access = [Owner]
+ },
+ Fun(FDI, Acc0);
+ (_Else, Acc0) ->
+ {ok, Acc0} % ignore for now
+ end,
+ VName = <<"_access_by_seq">>,
+ query_view(Db, DDoc, VName, Args, Callback, Acc).
+
+query_all_docs_access(Db, Args0, Callback0, Acc) ->
+ % query our not yest existing, home-grown _access view.
+ % use query_view for this.
+ DDoc = access_ddoc(),
+ UserCtx = couch_db:get_user_ctx(Db),
+ UserName = UserCtx#user_ctx.name,
+ Args1 = prefix_startkey_endkey(UserName, Args0, Args0#mrargs.direction),
+ Args = Args1#mrargs{reduce=false, extra=Args1#mrargs.extra ++ [{all_docs_access, true}]},
+ Callback = fun
+ ({row, Props}, Acc0) ->
+ % filter out the user-prefix from the key, so _all_docs looks normal
+ % this isn’t a separate function because I’m binding Callback0 and I
+ % don’t know the Erlang equivalent of JS’s fun.bind(this, newarg)
+ [_User, Key] = proplists:get_value(key, Props),
+ Row0 = proplists:delete(key, Props),
+ Row = [{key, Key} | Row0],
+ Callback0({row, Row}, Acc0);
+ (Row, Acc0) ->
+ Callback0(Row, Acc0)
+ end,
+ VName = <<"_access_by_id">>,
+ query_view(Db, DDoc, VName, Args, Callback, Acc).
+
+prefix_startkey_endkey(UserName, Args, fwd) ->
+ #mrargs{start_key=StartKey, end_key=EndKey} = Args,
+ Args#mrargs {
+ start_key = case StartKey of
+ undefined -> [UserName];
+ StartKey -> [UserName, StartKey]
+ end,
+ end_key = case EndKey of
+ undefined -> [UserName, {}];
+ EndKey -> [UserName, EndKey, {}]
+ end
+ };
+
+prefix_startkey_endkey(UserName, Args, rev) ->
+ #mrargs{start_key=StartKey, end_key=EndKey} = Args,
+ Args#mrargs {
+ end_key = case StartKey of
+ undefined -> [UserName];
+ StartKey -> [UserName, StartKey]
+ end,
+ start_key = case EndKey of
+ undefined -> [UserName, {}];
+ EndKey -> [UserName, EndKey, {}]
+ end
+ }.
+query_all_docs_admin(Db, Args0, Callback, Acc) ->
Sig = couch_util:with_db(Db, fun(WDb) ->
{ok, Info} = couch_db:get_db_info(WDb),
couch_index_util:hexsig(couch_hash:md5_hash(term_to_binary(Info)))
diff --git a/src/couch_mrview/src/couch_mrview_updater.erl b/src/couch_mrview/src/couch_mrview_updater.erl
index 969a82028..5d58ab05d 100644
--- a/src/couch_mrview/src/couch_mrview_updater.erl
+++ b/src/couch_mrview/src/couch_mrview_updater.erl
@@ -124,8 +124,9 @@ process_doc(Doc, Seq, #mrst{doc_acc = Acc} = State) when length(Acc) > 100 ->
process_doc(Doc, Seq, State#mrst{doc_acc = []});
process_doc(nil, Seq, #mrst{doc_acc = Acc} = State) ->
{ok, State#mrst{doc_acc = [{nil, Seq, nil} | Acc]}};
-process_doc(#doc{id = Id, deleted = true}, Seq, #mrst{doc_acc = Acc} = State) ->
- {ok, State#mrst{doc_acc = [{Id, Seq, deleted} | Acc]}};
+% TODO: re-evaluate why this is commented out
+% process_doc(#doc{id=Id, deleted=true}, Seq, #mrst{doc_acc=Acc}=State) ->
+% {ok, State#mrst{doc_acc=[{Id, Seq, deleted} | Acc]}};
process_doc(#doc{id = Id} = Doc, Seq, #mrst{doc_acc = Acc} = State) ->
{ok, State#mrst{doc_acc = [{Id, Seq, Doc} | Acc]}}.
@@ -149,6 +150,14 @@ finish_update(#mrst{doc_acc = Acc} = State) ->
}}
end.
+make_deleted_body({Props}, Meta, Seq) ->
+ BodySp = couch_util:get_value(body_sp, Meta),
+ Result = [{<<"_seq">>, Seq}, {<<"_body_sp">>, BodySp}],
+ case couch_util:get_value(<<"_access">>, Props) of
+ undefined -> Result;
+ Access -> [{<<"_access">>, Access} | Result]
+ end.
+
map_docs(Parent, #mrst{db_name = DbName, idx_name = IdxName} = State0) ->
erlang:put(io_priority, {view_update, DbName, IdxName}),
case couch_work_queue:dequeue(State0#mrst.doc_queue) of
@@ -167,11 +176,38 @@ map_docs(Parent, #mrst{db_name = DbName, idx_name = IdxName} = State0) ->
DocFun = fun
({nil, Seq, _}, {SeqAcc, Results}) ->
{erlang:max(Seq, SeqAcc), Results};
- ({Id, Seq, deleted}, {SeqAcc, Results}) ->
- {erlang:max(Seq, SeqAcc), [{Id, []} | Results]};
+ ({Id, Seq, Rev, #doc{deleted=true, body=Body, meta=Meta}}, {SeqAcc, Results}) ->
+ % _access needs deleted docs
+ case IdxName of
+ <<"_design/_access">> ->
+ % splice in seq
+ {Start, Rev1} = Rev,
+ Doc = #doc{
+ id = Id,
+ revs = {Start, [Rev1]},
+ body = {make_deleted_body(Body, Meta, Seq)}, %% todo: only keep _access and add _seq
+ deleted = true
+ },
+ {ok, Res} = couch_query_servers:map_doc_raw(QServer, Doc),
+ {erlang:max(Seq, SeqAcc), [{Id, Seq, Rev, Res} | Results]};
+ _Else ->
+ {erlang:max(Seq, SeqAcc), [{Id, Seq, Rev, []} | Results]}
+ end;
({Id, Seq, Doc}, {SeqAcc, Results}) ->
couch_stats:increment_counter([couchdb, mrview, map_doc]),
- {ok, Res} = couch_query_servers:map_doc_raw(QServer, Doc),
+ % IdxName: ~p, Doc: ~p~n~n", [IdxName, Doc]),
+ Doc0 = case IdxName of
+ <<"_design/_access">> ->
+ % splice in seq
+ {Props} = Doc#doc.body,
+ BodySp = couch_util:get_value(body_sp, Doc#doc.meta),
+ Doc#doc{
+ body = {Props++[{<<"_seq">>, Seq}, {<<"_body_sp">>, BodySp}]}
+ };
+ _Else ->
+ Doc
+ end,
+ {ok, Res} = couch_query_servers:map_doc_raw(QServer, Doc0),
{erlang:max(Seq, SeqAcc), [{Id, Res} | Results]}
end,
FoldFun = fun(Docs, Acc) ->
diff --git a/src/couch_mrview/src/couch_mrview_util.erl b/src/couch_mrview/src/couch_mrview_util.erl
index e1e75f34f..7991487ff 100644
--- a/src/couch_mrview/src/couch_mrview_util.erl
+++ b/src/couch_mrview/src/couch_mrview_util.erl
@@ -21,6 +21,7 @@
-export([index_file/2, compaction_file/2, open_file/1]).
-export([delete_files/2, delete_index_file/2, delete_compaction_file/2]).
-export([get_row_count/1, all_docs_reduce_to_count/1, reduce_to_count/1]).
+-export([get_access_row_count/2]).
-export([all_docs_key_opts/1, all_docs_key_opts/2, key_opts/1, key_opts/2]).
-export([fold/4, fold_reduce/4]).
-export([temp_view_to_ddoc/1]).
@@ -437,6 +438,11 @@ reduce_to_count(Reductions) ->
FinalReduction = couch_btree:final_reduce(CountReduceFun, Reductions),
get_count(FinalReduction).
+get_access_row_count(#mrview{btree=Bt}, UserName) ->
+ couch_btree:full_reduce_with_options(Bt, [
+ {start_key, UserName}
+ ]).
+
fold(#mrview{btree = Bt}, Fun, Acc, Opts) ->
WrapperFun = fun(KV, Reds, Acc2) ->
fold_fun(Fun, expand_dups([KV], []), Reds, Acc2)
@@ -479,8 +485,9 @@ validate_args(#mrst{} = State, Args0) ->
ViewPartitioned = State#mrst.partitioned,
Partition = get_extra(Args, partition),
+ AllDocsAccess = get_extra(Args, all_docs_access, false),
- case {ViewPartitioned, Partition} of
+ case {ViewPartitioned and not AllDocsAccess, Partition} of
{true, undefined} ->
Msg1 = <<
"`partition` parameter is mandatory "