rnewson commented on code in PR #4673:
URL: https://github.com/apache/couchdb/pull/4673#discussion_r1285854728
##########
src/chttpd/src/chttpd_db.erl:
##########
@@ -1967,6 +1972,21 @@ parse_shards_opt("placement", Req, Default) ->
throw({bad_request, Err})
end
end;
+parse_shards_opt("access", Req, Value) when is_list(Value) ->
Review Comment:
this clause seems unnecessary. handle "true" and "false" in the next two
clauses and the final clause will throw an error for anything else.
##########
rel/overlay/etc/default.ini:
##########
@@ -402,6 +402,10 @@ authentication_db = _users
; max_iterations, password_scheme, password_regexp, proxy_use_secret,
; public_fields, secret, users_db_public, cookie_domain, same_site
+; Per document access settings
+[per_doc_access]
+;enabled = false
Review Comment:
`enable` is established over `enabled` in a few other spots.
##########
src/chttpd/src/chttpd_db.erl:
##########
@@ -1967,6 +1972,21 @@ parse_shards_opt("placement", Req, Default) ->
throw({bad_request, Err})
end
end;
+parse_shards_opt("access", Req, Value) when is_list(Value) ->
+ parse_shards_opt("access", Req, list_to_existing_atom(Value));
+parse_shards_opt("access", _Req, Value) when Value =:= true ->
Review Comment:
clearer to just match directly? `parse_shards_opt("access", _Req, "true")`
##########
src/chttpd/src/chttpd_db.erl:
##########
@@ -1967,6 +1972,21 @@ parse_shards_opt("placement", Req, Default) ->
throw({bad_request, Err})
end
end;
+parse_shards_opt("access", Req, Value) when is_list(Value) ->
+ parse_shards_opt("access", Req, list_to_existing_atom(Value));
+parse_shards_opt("access", _Req, Value) when Value =:= true ->
+ case config:get_boolean("per_doc_access", "enabled", false) of
+ true ->
+ true;
+ false ->
+ Err = ?l2b(["The `access` option is not available on this CouchDB
installation."]),
Review Comment:
since it's static, just declare it as a binary directly `Err = <<"The...
">>,`
##########
src/chttpd/src/chttpd_db.erl:
##########
@@ -958,16 +958,18 @@ view_cb(Msg, Acc) ->
couch_mrview_http:view_cb(Msg, Acc).
db_doc_req(#httpd{method = 'DELETE'} = Req, Db, DocId) ->
- % check for the existence of the doc to handle the 404 case.
- couch_doc_open(Db, DocId, nil, []),
- case chttpd:qs_value(Req, "rev") of
+ % fetch the old doc revision, so we can compare access control
+ % in send_update_doc() later.
+ Doc0 = couch_doc_open(Db, DocId, nil, [{user_ctx, Req#httpd.user_ctx}]),
+ Revs = chttpd:qs_value(Req, "rev"),
Review Comment:
it confused me that this is called `Revs` when it is either undefined or one
rev.
##########
src/couch/src/couch_access_native_proc.erl:
##########
@@ -0,0 +1,139 @@
+% 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_access_native_proc).
+-behavior(gen_server).
+
+-export([
+ start_link/0,
+ set_timeout/2,
+ prompt/2
+]).
+
+-export([
+ init/1,
+ terminate/2,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ code_change/3
+]).
+
+-record(st, {
+ indexes = [],
+ % TODO: make configurable
+ timeout = 5000
+}).
+
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+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}).
+
+init(_) ->
+ {ok, #st{}}.
+
+terminate(_Reason, _St) ->
+ ok.
+
+handle_call({set_timeout, TimeOut}, _From, St) ->
+ {reply, ok, St#st{timeout = TimeOut}};
+handle_call({prompt, [<<"reset">>]}, _From, St) ->
+ {reply, true, St#st{indexes = []}};
+handle_call({prompt, [<<"reset">>, _QueryConfig]}, _From, St) ->
+ {reply, true, St#st{indexes = []}};
+handle_call({prompt, [<<"add_fun">>, IndexInfo]}, _From, St) ->
+ {reply, true, St};
+handle_call({prompt, [<<"map_doc">>, Doc]}, _From, St) ->
+ {reply, map_doc(St, mango_json:to_binary(Doc)), St};
+handle_call({prompt, [<<"reduce">>, _, _]}, _From, St) ->
+ {reply, null, St};
+handle_call({prompt, [<<"rereduce">>, _, _]}, _From, St) ->
+ {reply, null, St};
+handle_call({prompt, [<<"index_doc">>, Doc]}, _From, St) ->
+ {reply, [[]], St};
+handle_call(Msg, _From, St) ->
+ {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
+
+handle_cast(garbage_collect, St) ->
+ erlang:garbage_collect(),
+ {noreply, St};
+handle_cast(Msg, St) ->
+ {stop, {invalid_cast, Msg}, St}.
+
+handle_info(Msg, St) ->
+ {stop, {invalid_info, Msg}, St}.
+
+code_change(_OldVsn, St, _Extra) ->
+ {ok, St}.
+
+% Return value is an array of arrays, first dimension is the different indexes
+% [0] will be by-access-id // for this test, later we should make this
by-access
+% -seq, since that one we will always need, and by-access-id can be opt-in.
+% the second dimension is the number of emit kv pairs:
+% [ // the return value
+% [ // the first view
+% ['k1', 'v1'], // the first k/v pair for the first view
+% ['k2', 'v2'] // second, etc.
+% ],
+% [ // second view
+% ['l1', 'w1'] // first k/v par in second view
+% ]
+% ]
+%
{"id":"account/bongel","key":"account/bongel","value":{"rev":"1-967a00dff5e02add41819138abb3284d"}},
+
+map_doc(_St, {Doc}) ->
+ case couch_util:get_value(<<"_access">>, Doc) of
+ undefined ->
+ % do not index this doc
+ [[], []];
+ Access when is_list(Access) ->
+ Id = couch_util:get_value(<<"_id">>, Doc),
+ Rev = couch_util:get_value(<<"_rev">>, Doc),
+ Seq = couch_util:get_value(<<"_seq">>, Doc),
+ Deleted = couch_util:get_value(<<"_deleted">>, Doc, false),
+ BodySp = couch_util:get_value(<<"_body_sp">>, Doc),
+ % by-access-id
+ ById =
+ case Deleted of
+ false ->
+ lists:map(
+ fun(UserOrRole) ->
+ [
+ [[UserOrRole, Id], Rev]
+ ]
+ end,
+ Access
+ );
+ _True ->
+ [[]]
+ end,
+
+ % by-access-seq
+ BySeq = lists:map(
+ fun(UserOrRole) ->
+ [
+ [[UserOrRole, Seq], [{rev, Rev}, {deleted, Deleted},
{body_sp, BodySp}]]
+ ]
+ end,
+ Access
+ ),
+ ById ++ BySeq;
+ Else ->
+ % TODO: no comprende: should not be needed once we implement
Review Comment:
needs fixing before merge. also if `Else` is ignored, should be `_Else`.
##########
src/couch/include/couch_db.hrl:
##########
@@ -67,7 +67,8 @@
-record(doc_info, {
id = <<"">>,
high_seq = 0,
- revs = [] % rev_info
+ revs = [], % rev_info
+ access = []
Review Comment:
is `doc_info` this passed between nodes?
##########
src/couch/src/couch_db.erl:
##########
@@ -302,17 +312,23 @@ open_doc(Db, Id, Options) ->
{ok, #doc{deleted = true} = Doc} ->
case lists:member(deleted, Options) of
true ->
- apply_open_options({ok, Doc}, Options);
Review Comment:
why is this removed?
##########
src/couch/src/couch_db.erl:
##########
@@ -769,6 +787,64 @@ security_error_type(#user_ctx{name = null}) ->
security_error_type(#user_ctx{name = _}) ->
forbidden.
+is_per_user_ddoc(#doc{access = []}) -> false;
+is_per_user_ddoc(#doc{access = [<<"_users">>]}) -> false;
Review Comment:
the name of the users db is configurable, isn't it?
##########
src/couch/src/couch_db.erl:
##########
@@ -137,6 +140,7 @@
]).
-include_lib("couch/include/couch_db.hrl").
+
Review Comment:
?
##########
src/couch/src/couch_db.erl:
##########
@@ -769,6 +787,64 @@ security_error_type(#user_ctx{name = null}) ->
security_error_type(#user_ctx{name = _}) ->
forbidden.
+is_per_user_ddoc(#doc{access = []}) -> false;
+is_per_user_ddoc(#doc{access = [<<"_users">>]}) -> false;
+is_per_user_ddoc(_) -> true.
+
+validate_access(Db, Doc) ->
+ validate_access(Db, Doc, []).
+
+validate_access(Db, Doc, Options) ->
+ validate_access1(has_access_enabled(Db), Db, Doc, Options).
+
+validate_access1(false, _Db, _Doc, _Options) ->
+ ok;
+validate_access1(true, Db, #doc{id = <<"_design", _/binary>>} = Doc, Options)
->
+ case is_read_from_ddoc_cache(Options) andalso is_per_user_ddoc(Doc) of
+ true -> throw({not_found, missing});
+ _False -> validate_access2(Db, Doc)
+ end;
+validate_access1(true, Db, #doc{} = Doc, _Options) ->
+ validate_access2(Db, Doc).
+validate_access2(Db, Doc) ->
+ validate_access3(check_access(Db, Doc)).
+
+validate_access3(true) -> ok;
+validate_access3(_) -> throw({forbidden, <<"access denied">>}).
+
+check_access(Db, #doc{access = Access}) ->
+ check_access(Db, Access);
+check_access(Db, Access) ->
+ #user_ctx{
+ name = UserName,
+ roles = UserRoles
+ } = Db#db.user_ctx,
+ case Access of
+ [] ->
+ % if doc has no _access, userCtX must be admin
+ is_admin(Db);
+ Access ->
+ % if doc has _access, userCtx must be admin OR matching user or
role
+ case is_admin(Db) of
+ true ->
+ true;
+ _ ->
+ case {check_name(UserName, Access), check_roles(UserRoles,
Access)} of
Review Comment:
`check_name andalso check_roles` then we just need a 'true' and 'false'
case. If not that, at least clarify the other cases and not just `_`
##########
src/couch/src/couch_db.erl:
##########
@@ -1458,23 +1567,29 @@ collect_results(Pid, MRef, ResultsAcc) ->
end.
write_and_commit(
- #db{main_pid = Pid, user_ctx = Ctx} = Db,
+ #db{main_pid = Pid, user_ctx = UserCtx0} = Db,
DocBuckets1,
LocalDocs,
Options
) ->
DocBuckets = prepare_doc_summaries(Db, DocBuckets1),
ReplicatedChanges = lists:member(?REPLICATED_CHANGES, Options),
MRef = erlang:monitor(process, Pid),
+ UserCtx =
+ case has_access_enabled(Db) of
+ true -> UserCtx0;
+ false -> []
+ end,
+
try
- Pid ! {update_docs, self(), DocBuckets, LocalDocs, ReplicatedChanges},
+ Pid ! {update_docs, self(), DocBuckets, LocalDocs, ReplicatedChanges,
UserCtx},
Review Comment:
note to self to check update_docs receiving end for mixed clustered rpc
issues
##########
src/couch/src/couch_db.erl:
##########
@@ -1330,13 +1411,41 @@ update_docs(Db, Docs0, Options, ?REPLICATED_CHANGES) ->
]
|| Bucket <- DocBuckets
],
- {ok, _} = write_and_commit(
+ {ok, Results} = write_and_commit(
Db,
DocBuckets2,
LocalDocs,
[?REPLICATED_CHANGES | Options]
),
- {ok, DocErrors};
+ case couch_db:has_access_enabled(Db) of
+ false ->
+ % we’re done here
+ {ok, DocErrors};
+ _ ->
Review Comment:
`s/_/true`
##########
src/couch/src/couch_db.erl:
##########
@@ -1330,13 +1411,41 @@ update_docs(Db, Docs0, Options, ?REPLICATED_CHANGES) ->
]
|| Bucket <- DocBuckets
],
- {ok, _} = write_and_commit(
+ {ok, Results} = write_and_commit(
Db,
DocBuckets2,
LocalDocs,
[?REPLICATED_CHANGES | Options]
),
- {ok, DocErrors};
+ case couch_db:has_access_enabled(Db) of
+ false ->
+ % we’re done here
+ {ok, DocErrors};
+ _ ->
+ AccessViolations = lists:filter(fun({_Ref, Tag}) -> Tag =:= access
end, Results),
+ case length(AccessViolations) of
+ 0 ->
+ % we’re done here
+ {ok, DocErrors};
+ _ ->
Review Comment:
`s/_/N when N > 0/`
##########
src/couch/src/couch_db.erl:
##########
@@ -1330,13 +1411,41 @@ update_docs(Db, Docs0, Options, ?REPLICATED_CHANGES) ->
]
|| Bucket <- DocBuckets
],
- {ok, _} = write_and_commit(
+ {ok, Results} = write_and_commit(
Db,
DocBuckets2,
LocalDocs,
[?REPLICATED_CHANGES | Options]
),
- {ok, DocErrors};
+ case couch_db:has_access_enabled(Db) of
+ false ->
+ % we’re done here
+ {ok, DocErrors};
+ _ ->
+ AccessViolations = lists:filter(fun({_Ref, Tag}) -> Tag =:= access
end, Results),
Review Comment:
`==` and `=:=` mean the same thing for atoms (they only differ when
evaluating numbers)
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -736,17 +768,91 @@ update_docs_int(Db, DocsList, LocalDocs,
ReplicatedChanges) ->
length(LocalDocs2)
),
- % Check if we just updated any design documents, and update the validation
- % funs if we did.
- UpdatedDDocIds = lists:flatmap(
- fun
- (<<"_design/", _/binary>> = Id) -> [Id];
- (_) -> []
+ % Check if we just updated any non-access design documents,
+ % and update the validation funs if we did.
+ UpdatedDDocIds = [
+ Id
+ || [{_Client, #doc{id = <<"_design/", _/binary>> = Id, access = []}, _} |
_] <- DocsList
+ ],
+ {ok, commit_data(Db1), UpdatedDDocIds}.
+
+% at this point, we already validated this Db is access enabled, so do the
checks right away.
+check_access(Db, UserCtx, Access) ->
+ couch_db:check_access(Db#db{user_ctx = UserCtx}, Access).
+
+validate_docs_access(Db, DocsList, OldDocInfos) ->
+ case couch_db:has_access_enabled(Db) of
+ true -> validate_docs_access_int(Db, DocsList, OldDocInfos);
+ _Else -> {DocsList, OldDocInfos}
Review Comment:
`s/_Else/false`
##########
src/couch/src/couch_httpd_auth.erl:
##########
@@ -199,7 +206,7 @@ proxy_auth_user(Req) ->
Roles =
case header_value(Req, XHeaderRoles) of
undefined -> [];
- Else -> re:split(Else, "\\s*,\\s*", [trim, {return,
binary}])
+ Else -> re:split(Else, "\\s*,\\s*", [trim, {return,
binary}]) ++ [<<"_users">>]
Review Comment:
`[<<"_users">> | re:split ...]`
##########
src/couch/src/couch_db.erl:
##########
@@ -302,17 +312,23 @@ open_doc(Db, Id, Options) ->
{ok, #doc{deleted = true} = Doc} ->
case lists:member(deleted, Options) of
true ->
- apply_open_options({ok, Doc}, Options);
+ {ok, Doc};
false ->
{not_found, deleted}
end;
Else ->
- apply_open_options(Else, Options)
Review Comment:
why is this removed?
##########
src/couch/src/couch_httpd_auth.erl:
##########
@@ -98,6 +98,13 @@ basic_name_pw(Req) ->
nil
end.
+extract_roles(UserProps) ->
+ Roles = couch_util:get_value(<<"roles">>, UserProps, []),
+ case lists:member(<<"_admin">>, Roles) of
+ true -> Roles;
+ _ -> Roles ++ [<<"_users">>]
Review Comment:
`[<<"_users">> | Roles]`
##########
src/couch/src/couch_httpd_auth.erl:
##########
@@ -248,7 +255,7 @@ jwt_authentication_handler(Req) ->
Req#httpd{
user_ctx = #user_ctx{
name = User,
- roles = Roles
+ roles = Roles ++ [<<"_users">>]
Review Comment:
`[<<"_users">> | Roles]`
##########
src/chttpd/src/chttpd.erl:
##########
@@ -1034,6 +1034,8 @@ error_info({bad_request, Error, Reason}) ->
{400, couch_util:to_binary(Error), couch_util:to_binary(Reason)};
error_info({query_parse_error, Reason}) ->
{400, <<"query_parse_error">>, Reason};
+error_info(access) ->
Review Comment:
this is too generic for me, `per_doc_access_denied` or something similarly
descriptive please.
##########
src/couch/src/couch_db.erl:
##########
@@ -910,9 +986,14 @@ group_alike_docs([Doc | Rest], [Bucket | RestBuckets]) ->
end.
validate_doc_update(#db{} = Db, #doc{id = <<"_design/", _/binary>>} = Doc,
_GetDiskDocFun) ->
- case catch check_is_admin(Db) of
- ok -> validate_ddoc(Db, Doc);
- Error -> Error
+ case couch_doc:has_access(Doc) of
+ true ->
+ validate_ddoc(Db, Doc);
+ _Else ->
Review Comment:
`s/_Else/false`
##########
src/couch/src/couch_access_native_proc.erl:
##########
@@ -0,0 +1,139 @@
+% 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_access_native_proc).
+-behavior(gen_server).
+
+-export([
+ start_link/0,
+ set_timeout/2,
+ prompt/2
+]).
+
+-export([
+ init/1,
+ terminate/2,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ code_change/3
+]).
+
+-record(st, {
+ indexes = [],
+ % TODO: make configurable
+ timeout = 5000
+}).
+
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+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}).
Review Comment:
is a 5 second timeout appropriate here?
##########
src/couch/src/couch_db.erl:
##########
@@ -1363,7 +1472,6 @@ update_docs_interactive(Db, Docs0, Options) ->
{ok, DocBuckets, LocalDocs, DocErrors} =
before_docs_update(Db, Docs, PrepValidateFun, ?INTERACTIVE_EDIT),
-
Review Comment:
?
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -294,12 +304,12 @@ collect_updates(GroupedDocsAcc, ClientsAcc,
ReplicatedChanges) ->
% local docs. It's easier to just avoid multiple _local doc
% updaters than deal with their possible conflicts, and local docs
% writes are relatively rare. Can be optmized later if really needed.
- {update_docs, Client, GroupedDocs, [], ReplicatedChanges} ->
Review Comment:
do we need a clause with the original message format for internode
compatibility during upgrade?
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -736,17 +768,91 @@ update_docs_int(Db, DocsList, LocalDocs,
ReplicatedChanges) ->
length(LocalDocs2)
),
- % Check if we just updated any design documents, and update the validation
- % funs if we did.
- UpdatedDDocIds = lists:flatmap(
- fun
- (<<"_design/", _/binary>> = Id) -> [Id];
- (_) -> []
+ % Check if we just updated any non-access design documents,
+ % and update the validation funs if we did.
+ UpdatedDDocIds = [
+ Id
+ || [{_Client, #doc{id = <<"_design/", _/binary>> = Id, access = []}, _} |
_] <- DocsList
+ ],
+ {ok, commit_data(Db1), UpdatedDDocIds}.
+
+% at this point, we already validated this Db is access enabled, so do the
checks right away.
+check_access(Db, UserCtx, Access) ->
+ couch_db:check_access(Db#db{user_ctx = UserCtx}, Access).
+
+validate_docs_access(Db, DocsList, OldDocInfos) ->
+ case couch_db:has_access_enabled(Db) of
+ true -> validate_docs_access_int(Db, DocsList, OldDocInfos);
+ _Else -> {DocsList, OldDocInfos}
+ end.
+
+validate_docs_access_int(Db, DocsList, OldDocInfos) ->
+ validate_docs_access(Db, DocsList, OldDocInfos, [], []).
+
+validate_docs_access(_Db, [], [], DocsListValidated, OldDocInfosValidated) ->
+ % TODO: check if need to reverse this? maybe this is the cause of the test
reverse issue?
Review Comment:
junk
##########
src/couch/src/couch_db.erl:
##########
@@ -1446,6 +1554,7 @@ collect_results_with_metrics(Pid, MRef, []) ->
end.
collect_results(Pid, MRef, ResultsAcc) ->
+ % TDOD: need to receiver access?
Review Comment:
needs an answer
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -721,7 +751,9 @@ update_docs_int(Db, DocsList, LocalDocs, ReplicatedChanges)
->
% the trees, the attachments are already written to disk)
{ok, IndexFDIs} = flush_trees(Db, NewFullDocInfos, []),
Pairs = pair_write_info(OldDocLookups, IndexFDIs),
- LocalDocs2 = update_local_doc_revs(LocalDocs),
+ % TODO: local docs access needs validating
Review Comment:
is this TODO todone already?
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -711,7 +731,17 @@ update_docs_int(Db, DocsList, LocalDocs,
ReplicatedChanges) ->
cur_seq = UpdateSeq,
full_partitions = FullPartitions
},
- {ok, AccOut} = merge_rev_trees(DocsList, OldDocInfos, AccIn),
+ % Loop over DocsList, validate_access for each OldDocInfo on Db,
+ %. if no OldDocInfo, then send to DocsListValidated, keep OldDocsInfo
+ % if valid, then send to DocsListValidated, OldDocsInfo
+ %. if invalid, then send_result tagged `access`(c.f. `conflict)
+ %. and don’t add to DLV, nor ODI
+
+ {DocsListValidated, OldDocInfosValidated} = validate_docs_access(
+ Db, DocsList, OldDocInfos
+ ),
+ % couch_log:notice("~n~n u_d_i: DocsList: ~p~n, OldDocInfos: ~p~n,
DocsListValidated: ~p~n, OldDocInfosValidated: ~p~n~n~n", [DocsList,
OldDocInfos, DocsListValidated, OldDocInfosValidated]),
Review Comment:
junk
##########
src/couch/src/couch_db_updater.erl:
##########
@@ -736,17 +768,91 @@ update_docs_int(Db, DocsList, LocalDocs,
ReplicatedChanges) ->
length(LocalDocs2)
),
- % Check if we just updated any design documents, and update the validation
- % funs if we did.
- UpdatedDDocIds = lists:flatmap(
- fun
- (<<"_design/", _/binary>> = Id) -> [Id];
- (_) -> []
+ % Check if we just updated any non-access design documents,
+ % and update the validation funs if we did.
+ UpdatedDDocIds = [
+ Id
+ || [{_Client, #doc{id = <<"_design/", _/binary>> = Id, access = []}, _} |
_] <- DocsList
+ ],
+ {ok, commit_data(Db1), UpdatedDDocIds}.
+
+% at this point, we already validated this Db is access enabled, so do the
checks right away.
+check_access(Db, UserCtx, Access) ->
+ couch_db:check_access(Db#db{user_ctx = UserCtx}, Access).
+
+validate_docs_access(Db, DocsList, OldDocInfos) ->
+ case couch_db:has_access_enabled(Db) of
+ true -> validate_docs_access_int(Db, DocsList, OldDocInfos);
+ _Else -> {DocsList, OldDocInfos}
+ end.
+
+validate_docs_access_int(Db, DocsList, OldDocInfos) ->
+ validate_docs_access(Db, DocsList, OldDocInfos, [], []).
+
+validate_docs_access(_Db, [], [], DocsListValidated, OldDocInfosValidated) ->
+ % TODO: check if need to reverse this? maybe this is the cause of the test
reverse issue?
+ % {lists:reverse(DocsListValidated), lists:reverse(OldDocInfosValidated)};
+ {lists:reverse(DocsListValidated), lists:reverse(OldDocInfosValidated)};
+validate_docs_access(
+ Db, [Docs | DocRest], [OldInfo | OldInfoRest], DocsListValidated,
OldDocInfosValidated
+) ->
+ % loop over Docs as {Client, NewDoc}
+ % validate Doc
+ % if valid, then put back in Docs
+ % if not, then send_result and skip
+ NewDocs = lists:foldl(
+ fun({Client, Doc, UserCtx}, Acc) ->
+ % check if we are allowed to update the doc, skip when new doc
+ OldDocMatchesAccess =
+ case OldInfo#full_doc_info.rev_tree of
+ [] -> true;
+ _ -> check_access(Db, UserCtx,
OldInfo#full_doc_info.access)
+ end,
+
+ NewDocMatchesAccess = check_access(Db, UserCtx, Doc#doc.access),
+
+ case OldDocMatchesAccess andalso NewDocMatchesAccess of
+ % if valid, then send to DocsListValidated, OldDocsInfo
+ true ->
+ % and store the access context on the new doc
+ [{Client, Doc, UserCtx} | Acc];
+ % if invalid, then send_result tagged `access`(c.f. `conflict)
+ false ->
+ % and don’t add to DLV, nor ODI
+ send_result(Client, Doc, access),
+ Acc
+ end
end,
- Ids
+ [],
+ Docs
),
- {ok, commit_data(Db1), UpdatedDDocIds}.
+ {NewDocsListValidated, NewOldDocInfosValidated} =
+ %TODO: what if only 2/3?
+ case length(NewDocs) of
+ % we sent out all docs as invalid access, drop the old doc info
associated with it
+ 0 ->
+ {DocsListValidated, OldDocInfosValidated};
+ _ ->
Review Comment:
`s/_/N when N > 0/`
--
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]