From 4927832550bdb77d8883dd870086ae9cca51e075 Mon Sep 17 00:00:00 2001 From: Jan Lehnardt Date: Sun, 24 May 2020 17:14:05 +0200 Subject: feat: add _access to local docs, teach replicator to add _access --- src/couch/src/couch_db.erl | 11 +- src/couch/src/couch_db_updater.erl | 10 +- src/couch/test/couchdb_access_tests.erl | 115 ++++++++++++--------- src/couch_replicator/src/couch_replicator.erl | 5 +- .../src/couch_replicator_api_wrap.erl | 2 +- src/couch_replicator/src/couch_replicator_ids.erl | 1 + .../src/couch_replicator_scheduler_job.erl | 39 +++++-- src/fabric/src/fabric_db_info.erl | 2 + 8 files changed, 120 insertions(+), 65 deletions(-) diff --git a/src/couch/src/couch_db.erl b/src/couch/src/couch_db.erl index 29e869727..a57dd4559 100644 --- a/src/couch/src/couch_db.erl +++ b/src/couch/src/couch_db.erl @@ -585,7 +585,8 @@ get_db_info(Db) -> name = Name, compactor_pid = Compactor, instance_start_time = StartTime, - committed_update_seq = CommittedUpdateSeq + committed_update_seq = CommittedUpdateSeq, + access = Access } = Db, {ok, DocCount} = get_doc_count(Db), {ok, DelDocCount} = get_del_doc_count(Db), @@ -624,7 +625,8 @@ get_db_info(Db) -> {disk_format_version, DiskVersion}, {committed_update_seq, CommittedUpdateSeq}, {compacted_seq, CompactedSeq}, - {uuid, Uuid} + {uuid, Uuid}, + {access, Access} ], {ok, InfoList}. @@ -1765,7 +1767,10 @@ open_doc_revs_int(Db, IdRevs, Options) -> open_doc_int(Db, <> = Id, Options) -> case couch_db_engine:open_local_docs(Db, [Id]) of [#doc{} = Doc] -> - apply_open_options(Db, {ok, Doc}, Options); + couch_log:debug("~n===========================Doc: ~p~n", [Doc]), + { Body } = Doc#doc.body, + Access = couch_util:get_value(<<"_access">>, Body), + apply_open_options(Db, {ok, Doc#doc{access = Access}}, Options); [not_found] -> {not_found, missing} end; diff --git a/src/couch/src/couch_db_updater.erl b/src/couch/src/couch_db_updater.erl index 9853fb52a..b73e93082 100644 --- a/src/couch/src/couch_db_updater.erl +++ b/src/couch/src/couch_db_updater.erl @@ -614,7 +614,8 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts, FullCommit) -> % 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), + LocalDocs1 = apply_local_docs_access(LocalDocs), + LocalDocs2 = update_local_doc_revs(LocalDocs1), {ok, Db1} = couch_db_engine:write_doc_infos(Db, Pairs, LocalDocs2), WriteCount = length(IndexFDIs), @@ -636,7 +637,12 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts, FullCommit) -> {ok, commit_data(Db1, not FullCommit), UpdatedDDocIds}. - +apply_local_docs_access(Docs) -> + lists:map(fun({Client, #doc{access = Access, body = {Body}} = Doc}) -> + Doc1 = Doc#doc{body = {[{<<"_access">>, Access} | Body]}}, + {Client, Doc1} + end, Docs). + update_local_doc_revs(Docs) -> lists:foldl(fun({Client, Doc}, Acc) -> case increment_local_doc_revs(Doc) of diff --git a/src/couch/test/couchdb_access_tests.erl b/src/couch/test/couchdb_access_tests.erl index e771b6c0b..8aedd34ef 100644 --- a/src/couch/test/couchdb_access_tests.erl +++ b/src/couch/test/couchdb_access_tests.erl @@ -42,6 +42,7 @@ before_all() -> Couch = test_util:start_couch([chttpd, couch_replicator]), Hashed = couch_passwords:hash_admin_password("a"), ok = config:set("admins", "a", binary_to_list(Hashed), _Persist=false), + ok = config:set("couchdb", "uuid", "21ac467c1bc05e9d9e9d2d850bb1108f", _Persist=false), ok = config:set("log", "level", "debug", _Persist=false), % cleanup and setup @@ -68,58 +69,58 @@ after_all(_) -> access_test_() -> Tests = [ % Doc creation - % fun should_not_let_anonymous_user_create_doc/2, - % fun should_let_admin_create_doc_with_access/2, - % fun should_let_admin_create_doc_without_access/2, - % fun should_let_user_create_doc_for_themselves/2, - % fun should_not_let_user_create_doc_for_someone_else/2, - % fun should_let_user_create_access_ddoc/2, - % fun access_ddoc_should_have_no_effects/2, - % - % % Doc updates - % fun users_with_access_can_update_doc/2, - % fun users_with_access_can_not_change_access/2, - % fun users_with_access_can_not_remove_access/2, - % - % % Doc reads - % fun should_let_admin_read_doc_with_access/2, - % fun user_with_access_can_read_doc/2, - % fun user_without_access_can_not_read_doc/2, - % fun user_can_not_read_doc_without_access/2, - % fun admin_with_access_can_read_conflicted_doc/2, - % fun user_with_access_can_not_read_conflicted_doc/2, - % - % % Doc deletes - % fun should_let_admin_delete_doc_with_access/2, - % fun should_let_user_delete_doc_for_themselves/2, - % fun should_not_let_user_delete_doc_for_someone_else/2, - % - % % _all_docs with include_docs - % fun should_let_admin_fetch_all_docs/2, - % fun should_let_user_fetch_their_own_all_docs/2, - % % % potential future feature - % % % fun should_let_user_fetch_their_own_all_docs_plus_users_ddocs/2%, - % - % % _changes - % fun should_let_admin_fetch_changes/2, - % fun should_let_user_fetch_their_own_changes/2, - % - % % views - % fun should_not_allow_admin_access_ddoc_view_request/2, - % fun should_not_allow_user_access_ddoc_view_request/2, - % fun should_allow_admin_users_access_ddoc_view_request/2, - % fun should_allow_user_users_access_ddoc_view_request/2, + fun should_not_let_anonymous_user_create_doc/2, + fun should_let_admin_create_doc_with_access/2, + fun should_let_admin_create_doc_without_access/2, + fun should_let_user_create_doc_for_themselves/2, + fun should_not_let_user_create_doc_for_someone_else/2, + fun should_let_user_create_access_ddoc/2, + fun access_ddoc_should_have_no_effects/2, + + % Doc updates + fun users_with_access_can_update_doc/2, + fun users_with_access_can_not_change_access/2, + fun users_with_access_can_not_remove_access/2, + + % Doc reads + fun should_let_admin_read_doc_with_access/2, + fun user_with_access_can_read_doc/2, + fun user_without_access_can_not_read_doc/2, + fun user_can_not_read_doc_without_access/2, + fun admin_with_access_can_read_conflicted_doc/2, + fun user_with_access_can_not_read_conflicted_doc/2, + + % Doc deletes + fun should_let_admin_delete_doc_with_access/2, + fun should_let_user_delete_doc_for_themselves/2, + fun should_not_let_user_delete_doc_for_someone_else/2, + + % _all_docs with include_docs + fun should_let_admin_fetch_all_docs/2, + fun should_let_user_fetch_their_own_all_docs/2, + % % potential future feature + % % fun should_let_user_fetch_their_own_all_docs_plus_users_ddocs/2%, + + % _changes + fun should_let_admin_fetch_changes/2, + fun should_let_user_fetch_their_own_changes/2, + + % views + fun should_not_allow_admin_access_ddoc_view_request/2, + fun should_not_allow_user_access_ddoc_view_request/2, + fun should_allow_admin_users_access_ddoc_view_request/2, + fun should_allow_user_users_access_ddoc_view_request/2, % replication - % fun should_allow_admin_to_replicate_from_access_to_access/2, - % fun should_allow_admin_to_replicate_from_no_access_to_access/2, - % fun should_allow_admin_to_replicate_from_access_to_no_access/2, - % fun should_allow_admin_to_replicate_from_no_access_to_no_access/2 + fun should_allow_admin_to_replicate_from_access_to_access/2, + fun should_allow_admin_to_replicate_from_no_access_to_access/2, + fun should_allow_admin_to_replicate_from_access_to_no_access/2, + fun should_allow_admin_to_replicate_from_no_access_to_no_access/2, - % fun should_allow_user_to_replicate_from_access_to_access/2, - % fun should_allow_user_to_replicate_from_access_to_no_access/2, - % fun should_allow_user_to_replicate_from_no_access_to_access/2, - % fun should_allow_user_to_replicate_from_no_access_to_no_access/2 + fun should_allow_user_to_replicate_from_access_to_access/2, + fun should_allow_user_to_replicate_from_access_to_no_access/2, + fun should_allow_user_to_replicate_from_no_access_to_access/2, + fun should_allow_user_to_replicate_from_no_access_to_no_access/2, % TODO: try getting _revs_diff for docs you don’t have access to fun should_not_allow_user_to_revs_diff_other_docs/2 @@ -719,11 +720,13 @@ should_allow_user_to_replicate_from_access_to_access(_PortType, Url) -> ]}, {ok, ResponseCode, _, ResponseBody} = test_request:post(Url ++ "/_replicate", ?USERX_REQ_HEADERS, jiffy:encode(EJRequestBody)), + % ?debugFmt("~nResponseBody: ~p~n", [ResponseBody]), % assert replication status {EJResponseBody} = jiffy:decode(ResponseBody), ?assertEqual(ResponseCode, 200), ?assertEqual(true, couch_util:get_value(<<"ok">>, EJResponseBody)), + [{History}] = couch_util:get_value(<<"history">>, EJResponseBody), MissingChecked = couch_util:get_value(<<"missing_checked">>, History), @@ -738,6 +741,18 @@ should_allow_user_to_replicate_from_access_to_access(_PortType, Url) -> ?assertEqual(2, DocsWritten), ?assertEqual(0, DocWriteFailures), + % assert access in local doc + ReplicationId = couch_util:get_value(<<"replication_id">>, EJResponseBody), + {ok, 200, _, CheckPoint} = test_request:get(Url ++ "/db/_local/" ++ ReplicationId, + ?USERX_REQ_HEADERS), + {EJCheckPoint} = jiffy:decode(CheckPoint), + Access = couch_util:get_value(<<"_access">>, EJCheckPoint), + ?assertEqual([<<"x">>], Access), + + % make sure others can’t read our local docs + {ok, 403, _, _} = test_request:get(Url ++ "/db/_local/" ++ ReplicationId, + ?USERY_REQ_HEADERS), + % assert docs in target db {ok, 200, _, ADBody} = test_request:get(Url ++ "/db2/_all_docs?include_docs=true", ?ADMIN_REQ_HEADERS), @@ -902,8 +917,6 @@ should_allow_user_to_replicate_from_no_access_to_no_access(_PortType, Url) -> {Json} = jiffy:decode(ADBody), ?assertEqual(2, proplists:get_value(<<"total_rows">>, Json)) end). - - % revs_diff should_not_allow_user_to_revs_diff_other_docs(_PortType, Url) -> diff --git a/src/couch_replicator/src/couch_replicator.erl b/src/couch_replicator/src/couch_replicator.erl index 39141c301..1566ffd83 100644 --- a/src/couch_replicator/src/couch_replicator.erl +++ b/src/couch_replicator/src/couch_replicator.erl @@ -73,9 +73,10 @@ replicate(PostBody, Ctx) -> false -> check_authorization(RepId, UserCtx), {ok, Listener} = rep_result_listener(RepId), - Result = do_replication_loop(Rep), + {ok, {Result}} = do_replication_loop(Rep), couch_replicator_notifier:stop(Listener), - Result + {PublicRepId, _} = couch_replicator_ids:replication_id(Rep), % TODO: check with options + {ok, {[{<<"replication_id">>, ?l2b(PublicRepId)} | Result]}} end. diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index 44c290d33..90ec45bcb 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -362,7 +362,6 @@ open_doc(Db, Id, Options) -> {error, <<"not_found">>} end. - update_doc(Db, Doc, Options) -> update_doc(Db, Doc, Options, interactive_edit). @@ -386,6 +385,7 @@ update_doc(#httpdb{} = HttpDb, #doc{id = DocId} = Doc, Options, Type) -> [] end ++ [{"Content-Type", ?b2l(ContentType)}, {"Content-Length", Len}], Body = {fun stream_doc/1, {JsonBytes, Doc#doc.atts, Boundary, Len}}, + couch_log:debug("~nBody: ~p~n", [Body]), send_req( % A crash here bubbles all the way back up to run_user_fun inside % open_doc_revs, which will retry the whole thing. That's the diff --git a/src/couch_replicator/src/couch_replicator_ids.erl b/src/couch_replicator/src/couch_replicator_ids.erl index e10b98082..28cce0984 100644 --- a/src/couch_replicator/src/couch_replicator_ids.erl +++ b/src/couch_replicator/src/couch_replicator_ids.erl @@ -41,6 +41,7 @@ replication_id(#rep{options = Options} = Rep) -> replication_id(#rep{user_ctx = UserCtx} = Rep, 4) -> UUID = couch_server:get_uuid(), + couch_log:debug("~nUUID: ~p~n", [UUID]), SrcInfo = get_v4_endpoint(UserCtx, Rep#rep.source), TgtInfo = get_v4_endpoint(UserCtx, Rep#rep.target), maybe_append_filters([UUID, SrcInfo, TgtInfo], Rep); diff --git a/src/couch_replicator/src/couch_replicator_scheduler_job.erl b/src/couch_replicator/src/couch_replicator_scheduler_job.erl index f669d464d..63ee96bcb 100644 --- a/src/couch_replicator/src/couch_replicator_scheduler_job.erl +++ b/src/couch_replicator/src/couch_replicator_scheduler_job.erl @@ -68,6 +68,8 @@ rep_starttime, src_starttime, tgt_starttime, + src_access, + tgt_access, timer, % checkpoint timer changes_queue, changes_manager, @@ -587,6 +589,9 @@ init_state(Rep) -> {ok, SourceInfo} = couch_replicator_api_wrap:get_db_info(Source), {ok, TargetInfo} = couch_replicator_api_wrap:get_db_info(Target), + couch_log:debug("~nSourceInfo: ~p~n", [SourceInfo]), + couch_log:debug("~nTargetInfo: ~p~n", [TargetInfo]), + [SourceLog, TargetLog] = find_and_migrate_logs([Source, Target], Rep), {StartSeq0, History} = compare_replication_logs(SourceLog, TargetLog), @@ -612,6 +617,8 @@ init_state(Rep) -> rep_starttime = StartTime, src_starttime = get_value(<<"instance_start_time">>, SourceInfo), tgt_starttime = get_value(<<"instance_start_time">>, TargetInfo), + src_access = get_value(<<"access">>, SourceInfo), + tgt_access = get_value(<<"access">>, TargetInfo), session_id = couch_uuids:random(), source_db_compaction_notifier = start_db_compaction_notifier(Source, self()), @@ -721,8 +728,10 @@ do_checkpoint(State) -> rep_starttime = ReplicationStartTime, src_starttime = SrcInstanceStartTime, tgt_starttime = TgtInstanceStartTime, + src_access = SrcAccess, + tgt_access = TgtAccess, stats = Stats, - rep_details = #rep{options = Options}, + rep_details = #rep{options = Options, user_ctx = UserCtx}, session_id = SessionId } = State, case commit_to_both(Source, Target) of @@ -778,9 +787,9 @@ do_checkpoint(State) -> try {SrcRevPos, SrcRevId} = update_checkpoint( - Source, SourceLog#doc{body = NewRepHistory}, source), + Source, SourceLog#doc{body = NewRepHistory}, SrcAccess, UserCtx, source), {TgtRevPos, TgtRevId} = update_checkpoint( - Target, TargetLog#doc{body = NewRepHistory}, target), + Target, TargetLog#doc{body = NewRepHistory}, TgtAccess, UserCtx, target), NewState = State#rep_state{ checkpoint_history = NewRepHistory, committed_seq = NewTsSeq, @@ -805,16 +814,34 @@ do_checkpoint(State) -> update_checkpoint(Db, Doc, DbType) -> + update_checkpoint(Db, Doc, false, #user_ctx{}, DbType). + +update_checkpoint(Db, Doc) -> + update_checkpoint(Db, Doc, false, #user_ctx{}). + +update_checkpoint(Db, Doc, Access, UserCtx, DbType) -> try - update_checkpoint(Db, Doc) + update_checkpoint(Db, Doc, Access, UserCtx) catch throw:{checkpoint_commit_failure, Reason} -> throw({checkpoint_commit_failure, <<"Error updating the ", (to_binary(DbType))/binary, " checkpoint document: ", (to_binary(Reason))/binary>>}) end. +update_checkpoint(Db, #doc{id = LogId} = Doc0, Access, UserCtx) -> + % UserCtx = couch_db:get_user_ctx(Db), + % couch_log:debug("~n~n~n~nUserCtx: ~p~n", [UserCtx]), + % if db has _access, then: + % get userCtx from replication and splice into doc _access + Doc = case Access of + true -> Doc0#doc{access = [UserCtx#user_ctx.name]}; + _False -> Doc0 + end, + couch_log:debug("~n++++++++++++++++:~n", []), + couch_log:debug("~nAccess: ~p~n", [Access]), + couch_log:debug("~nUserCtx: ~p~n", [UserCtx]), -update_checkpoint(Db, #doc{id = LogId, body = LogBody} = Doc) -> + couch_log:debug("~nDoc: ~p~n", [Doc]), try case couch_replicator_api_wrap:update_doc(Db, Doc, [delay_commit]) of {ok, PosRevId} -> @@ -822,7 +849,7 @@ update_checkpoint(Db, #doc{id = LogId, body = LogBody} = Doc) -> {error, Reason} -> throw({checkpoint_commit_failure, Reason}) end - catch throw:conflict -> + catch throw:conflict -> %TODO: splice in access case (catch couch_replicator_api_wrap:open_doc(Db, LogId, [ejson_body])) of {ok, #doc{body = LogBody, revs = {Pos, [RevId | _]}}} -> % This means that we were able to update successfully the diff --git a/src/fabric/src/fabric_db_info.erl b/src/fabric/src/fabric_db_info.erl index 97a31c237..a02185cba 100644 --- a/src/fabric/src/fabric_db_info.erl +++ b/src/fabric/src/fabric_db_info.erl @@ -112,6 +112,8 @@ merge_results(Info) -> [{disk_format_version, lists:max(X)} | Acc]; (cluster, [X], Acc) -> [{cluster, {X}} | Acc]; + (access, [X], Acc) -> + [{access, X} | Acc]; (_, _, Acc) -> Acc end, [{instance_start_time, <<"0">>}], Dict). -- cgit v1.2.1