You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2022/06/25 09:11:36 UTC

[couchdb] branch feat/access-2022 created (now 06956354c)

This is an automated email from the ASF dual-hosted git repository.

jan pushed a change to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git


      at 06956354c feat(access): add util functions

This branch includes the following new commits:

     new 3a2a9c611 feat(access): add access handling to chttpd
     new 511300a4e feat(access): add access to couch_db internal records
     new 7c3df79b8 feat(access): handle new records in couch_doc
     new cce80b958 feat(access): add new _users role for all authenticated users
     new bf78db93c feat(access): add access query server
     new a6f3f38a5 feat(access): expand couch_btree / bt_engine to handle access
     new 5608d49f2 feat(access): handle access in couch_db[_updater]
     new 06956354c feat(access): add util functions

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[couchdb] 08/08: feat(access): add util functions

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 06956354c0fe20e02bba425eb9dc38d4e2d3410f
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Sat Jun 25 11:10:19 2022 +0200

    feat(access): add util functions
---
 src/couch/src/couch_util.erl | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl
index d71fc059c..4b5ee274f 100644
--- a/src/couch/src/couch_util.erl
+++ b/src/couch/src/couch_util.erl
@@ -43,6 +43,7 @@
 -export([set_process_priority/2]).
 -export([hmac/3]).
 -export([version_to_binary/1]).
+-export([validate_design_access/1, validate_design_access/2]).
 
 -include_lib("couch/include/couch_db.hrl").
 
@@ -792,3 +793,16 @@ version_to_binary(Ver) when is_list(Ver) ->
     Ver1 = lists:reverse(lists:dropwhile(IsZero, lists:reverse(Ver))),
     Ver2 = [erlang:integer_to_list(N) || N <- Ver1],
     ?l2b(lists:join(".", Ver2)).
+
+validate_design_access(DDoc) ->
+    validate_design_access1(DDoc, true).
+
+validate_design_access(Db, DDoc) ->
+    validate_design_access1(DDoc, couch_db:has_access_enabled(Db)).
+
+validate_design_access1(_DDoc, false) -> ok;
+validate_design_access1(DDoc, true) ->
+    is_users_ddoc(DDoc).
+
+is_users_ddoc(#doc{access=[<<"_users">>]}) -> ok;
+is_users_ddoc(_) -> throw({forbidden, <<"per-user ddoc access">>}).


[couchdb] 04/08: feat(access): add new _users role for all authenticated users

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit cce80b9588a5876c128de01ee8c344554299d1e9
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 17:13:25 2022 +0200

    feat(access): add new _users role for all authenticated users
---
 src/couch/src/couch_httpd_auth.erl | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index 47096e120..eb3a1936f 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -99,6 +99,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">>]
+    end.
+
 default_authentication_handler(Req) ->
     default_authentication_handler(Req, couch_auth_cache).
 
@@ -117,7 +124,7 @@ default_authentication_handler(Req, AuthModule) ->
                             Req#httpd{
                                 user_ctx = #user_ctx{
                                     name = UserName,
-                                    roles = couch_util:get_value(<<"roles">>, UserProps, [])
+                                    roles = extract_roles(UserProps)
                                 }
                             };
                         false ->
@@ -189,7 +196,7 @@ proxy_auth_user(Req) ->
             Roles =
                 case header_value(Req, XHeaderRoles) of
                     undefined -> [];
-                    Else -> [?l2b(R) || R <- string:tokens(Else, ",")]
+                    Else -> [?l2b(R) || R <- string:tokens(Else, ",")] ++ [<<"_users">>]
                 end,
             case
                 chttpd_util:get_chttpd_auth_config_boolean(
@@ -326,9 +333,7 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq} = Req, AuthModule) ->
                                             Req#httpd{
                                                 user_ctx = #user_ctx{
                                                     name = ?l2b(User),
-                                                    roles = couch_util:get_value(
-                                                        <<"roles">>, UserProps, []
-                                                    )
+                                                    roles = extract_roles(UserProps)
                                                 },
                                                 auth = {FullSecret, TimeLeft < Timeout * 0.9}
                                             };
@@ -449,7 +454,7 @@ handle_session_req(#httpd{method = 'POST', mochi_req = MochiReq} = Req, AuthModu
                 {[
                     {ok, true},
                     {name, UserName},
-                    {roles, couch_util:get_value(<<"roles">>, UserProps, [])}
+                    {roles, extract_roles(UserProps)}
                 ]}
             );
         false ->


[couchdb] 03/08: feat(access): handle new records in couch_doc

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 7c3df79b84bfb59fe06a50ef8af8cbb3e0869c9a
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 17:01:04 2022 +0200

    feat(access): handle new records in couch_doc
---
 src/couch/src/couch_doc.erl | 44 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 37 insertions(+), 7 deletions(-)

diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl
index 5d44e456c..087f4d09b 100644
--- a/src/couch/src/couch_doc.erl
+++ b/src/couch/src/couch_doc.erl
@@ -26,6 +26,8 @@
 -export([with_ejson_body/1]).
 -export([is_deleted/1]).
 
+-export([has_access/1, has_no_access/1]).
+
 -include_lib("couch/include/couch_db.hrl").
 
 -spec to_path(#doc{}) -> path().
@@ -40,15 +42,28 @@ to_branch(Doc, [RevId | Rest]) ->
     [{RevId, ?REV_MISSING, to_branch(Doc, Rest)}].
 
 % helpers used by to_json_obj
+reduce_access({Access}) -> Access;
+reduce_access(Access) -> Access.
+
 to_json_rev(0, []) ->
     [];
 to_json_rev(Start, [FirstRevId | _]) ->
     [{<<"_rev">>, ?l2b([integer_to_list(Start), "-", revid_to_str(FirstRevId)])}].
 
-to_json_body(true, {Body}) ->
+% TODO: remove if we can
+% to_json_body(Del, Body) ->
+%     to_json_body(Del, Body, []).
+
+to_json_body(true, {Body}, []) ->
     Body ++ [{<<"_deleted">>, true}];
-to_json_body(false, {Body}) ->
-    Body.
+to_json_body(false, {Body}, []) ->
+    Body;
+to_json_body(true, {Body}, Access0) ->
+    Access = reduce_access(Access0),
+    Body ++ [{<<"_deleted">>, true}] ++ [{<<"_access">>, {Access}}];
+to_json_body(false, {Body}, Access0) ->
+    Access = reduce_access(Access0),
+    Body ++ [{<<"_access">>, Access}].
 
 to_json_revisions(Options, Start, RevIds0) ->
     RevIds =
@@ -138,14 +153,15 @@ doc_to_json_obj(
         deleted = Del,
         body = Body,
         revs = {Start, RevIds},
-        meta = Meta
+        meta = Meta,
+        access = Access
     } = Doc,
     Options
 ) ->
     {
         [{<<"_id">>, Id}] ++
             to_json_rev(Start, RevIds) ++
-            to_json_body(Del, Body) ++
+            to_json_body(Del, Body, Access) ++
             to_json_revisions(Options, Start, RevIds) ++
             to_json_meta(Meta) ++
             to_json_attachments(Doc#doc.atts, Options)
@@ -401,7 +417,7 @@ max_seq(Tree, UpdateSeq) ->
     end,
     couch_key_tree:fold(FoldFun, UpdateSeq, Tree).
 
-to_doc_info_path(#full_doc_info{id = Id, rev_tree = Tree, update_seq = FDISeq}) ->
+to_doc_info_path(#full_doc_info{id = Id, rev_tree = Tree, update_seq = FDISeq, access = Access}) ->
     RevInfosAndPath = [
         {rev_info(Node), Path}
      || {_Leaf, Path} = Node <-
@@ -419,7 +435,7 @@ to_doc_info_path(#full_doc_info{id = Id, rev_tree = Tree, update_seq = FDISeq})
     ),
     [{_RevInfo, WinPath} | _] = SortedRevInfosAndPath,
     RevInfos = [RevInfo || {RevInfo, _Path} <- SortedRevInfosAndPath],
-    {#doc_info{id = Id, high_seq = max_seq(Tree, FDISeq), revs = RevInfos}, WinPath}.
+    {#doc_info{id = Id, high_seq = max_seq(Tree, FDISeq), revs = RevInfos, access = Access}, WinPath}.
 
 rev_info({#leaf{} = Leaf, {Pos, [RevId | _]}}) ->
     #rev_info{
@@ -459,6 +475,20 @@ is_deleted(Tree) ->
             false
     end.
 
+get_access({Props}) ->
+    get_access(couch_doc:from_json_obj({Props}));
+get_access(#doc{access=Access}) ->
+    Access.
+
+has_access(Doc) ->
+    has_access1(get_access(Doc)).
+
+has_no_access(Doc) ->
+    not has_access1(get_access(Doc)).
+
+has_access1([]) -> false;
+has_access1(_) -> true.
+
 get_validate_doc_fun({Props}) ->
     get_validate_doc_fun(couch_doc:from_json_obj({Props}));
 get_validate_doc_fun(#doc{body = {Props}} = DDoc) ->


[couchdb] 05/08: feat(access): add access query server

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit bf78db93c7da5e686bcd16a46a32f846edf5fdfb
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 17:18:11 2022 +0200

    feat(access): add access query server
---
 src/couch/src/couch_access_native_proc.erl | 143 +++++++++++++++++++++++++++++
 src/couch/src/couch_proc_manager.erl       |   1 +
 2 files changed, 144 insertions(+)

diff --git a/src/couch/src/couch_access_native_proc.erl b/src/couch/src/couch_access_native_proc.erl
new file mode 100644
index 000000000..965b124de
--- /dev/null
+++ b/src/couch/src/couch_access_native_proc.erl
@@ -0,0 +1,143 @@
+% 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 = [],
+    timeout = 5000 % TODO: make configurable
+}).
+
+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
+            % _access field validation
+            [[],[]]
+    end.
diff --git a/src/couch/src/couch_proc_manager.erl b/src/couch/src/couch_proc_manager.erl
index 46765b339..f7903ebd4 100644
--- a/src/couch/src/couch_proc_manager.erl
+++ b/src/couch/src/couch_proc_manager.erl
@@ -104,6 +104,7 @@ init([]) ->
     ets:insert(?SERVERS, get_servers_from_env("COUCHDB_QUERY_SERVER_")),
     ets:insert(?SERVERS, get_servers_from_env("COUCHDB_NATIVE_QUERY_SERVER_")),
     ets:insert(?SERVERS, [{"QUERY", {mango_native_proc, start_link, []}}]),
+    ets:insert(?SERVERS, [{"_ACCESS", {couch_access_native_proc, start_link, []}}]),
     maybe_configure_erlang_native_servers(),
 
     {ok, #state{


[couchdb] 02/08: feat(access): add access to couch_db internal records

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 511300a4e53cb0a1626557ea656ca3568ba9831c
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 15:42:29 2022 +0200

    feat(access): add access to couch_db internal records
---
 src/couch/include/couch_db.hrl | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/couch/include/couch_db.hrl b/src/couch/include/couch_db.hrl
index 633beebd0..1b4fb942e 100644
--- a/src/couch/include/couch_db.hrl
+++ b/src/couch/include/couch_db.hrl
@@ -61,7 +61,8 @@
 -record(doc_info, {
     id = <<"">>,
     high_seq = 0,
-    revs = [] % rev_info
+    revs = [], % rev_info
+    access = []
 }).
 
 -record(size_info, {
@@ -74,7 +75,8 @@
     update_seq = 0,
     deleted = false,
     rev_tree = [],
-    sizes = #size_info{}
+    sizes = #size_info{},
+    access = []
 }).
 
 -record(httpd, {
@@ -118,7 +120,8 @@
 
     % key/value tuple of meta information, provided when using special options:
     % couch_db:open_doc(Db, Id, Options).
-    meta = []
+    meta = [],
+    access = []
 }).
 
 
@@ -201,7 +204,8 @@
     ptr,
     seq,
     sizes = #size_info{},
-    atts = []
+    atts = [],
+    access = []
 }).
 
 -record (fabric_changes_acc, {


[couchdb] 07/08: feat(access): handle access in couch_db[_updater]

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 5608d49f2b0744bd4c2869fe782f52de79a5740f
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 18:43:52 2022 +0200

    feat(access): handle access in couch_db[_updater]
---
 src/couch/src/couch_db.erl         | 219 ++++++++++++++++++++++++++++++++-----
 src/couch/src/couch_db_int.hrl     |   3 +-
 src/couch/src/couch_db_updater.erl | 154 +++++++++++++++++++++-----
 3 files changed, 319 insertions(+), 57 deletions(-)

diff --git a/src/couch/src/couch_db.erl b/src/couch/src/couch_db.erl
index c7f1c8b5f..cbe427faa 100644
--- a/src/couch/src/couch_db.erl
+++ b/src/couch/src/couch_db.erl
@@ -31,6 +31,9 @@
     is_admin/1,
     check_is_admin/1,
     check_is_member/1,
+    validate_access/2,
+    check_access/2,
+    has_access_enabled/1,
 
     name/1,
     get_after_doc_read_fun/1,
@@ -136,6 +139,7 @@
 ]).
 
 -include_lib("couch/include/couch_db.hrl").
+-include_lib("couch_mrview/include/couch_mrview.hrl"). % TODO: can we do without this?
 -include("couch_db_int.hrl").
 
 -define(DBNAME_REGEX,
@@ -285,6 +289,12 @@ wait_for_compaction(#db{main_pid = Pid} = Db, Timeout) ->
 is_compacting(DbName) ->
     couch_server:is_compacting(DbName).
 
+has_access_enabled(#db{access=true}) -> true;
+has_access_enabled(_) -> false.
+
+is_read_from_ddoc_cache(Options) ->
+    lists:member(ddoc_cache, Options).
+
 delete_doc(Db, Id, Revisions) ->
     DeletedDocs = [#doc{id = Id, revs = [Rev], deleted = true} || Rev <- Revisions],
     {ok, [Result]} = update_docs(Db, DeletedDocs, []),
@@ -293,23 +303,33 @@ delete_doc(Db, Id, Revisions) ->
 open_doc(Db, IdOrDocInfo) ->
     open_doc(Db, IdOrDocInfo, []).
 
-open_doc(Db, Id, Options) ->
+open_doc(Db, Id, Options0) ->
     increment_stat(Db, [couchdb, database_reads]),
+    Options = case has_access_enabled(Db) of
+        true -> Options0 ++ [conflicts];
+        _Else -> Options0
+    end,
     case open_doc_int(Db, Id, Options) of
         {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)
+            Else
     end.
 
-apply_open_options({ok, Doc}, Options) ->
+apply_open_options(Db, {ok, Doc}, Options) ->
+    ok = validate_access(Db, Doc, Options),
+    apply_open_options1({ok, Doc}, Options);
+apply_open_options(_Db, Else, _Options) ->
+    Else.
+
+apply_open_options1({ok, Doc}, Options) ->
     apply_open_options2(Doc, Options);
-apply_open_options(Else, _Options) ->
+apply_open_options1(Else, _Options) ->
     Else.
 
 apply_open_options2(Doc, []) ->
@@ -350,7 +370,7 @@ find_ancestor_rev_pos({RevPos, [RevId | Rest]}, AttsSinceRevs) ->
 open_doc_revs(Db, Id, Revs, Options) ->
     increment_stat(Db, [couchdb, database_reads]),
     [{ok, Results}] = open_doc_revs_int(Db, [{Id, Revs}], Options),
-    {ok, [apply_open_options(Result, Options) || Result <- Results]}.
+    {ok, [apply_open_options(Db, Result, Options) || Result <- Results]}.
 
 % Each returned result is a list of tuples:
 % {Id, MissingRevs, PossibleAncestors}
@@ -615,7 +635,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),
@@ -650,7 +671,8 @@ get_db_info(Db) ->
         {committed_update_seq, CommittedUpdateSeq},
         {compacted_seq, CompactedSeq},
         {props, Props},
-        {uuid, Uuid}
+        {uuid, Uuid},
+        {access, Access}
     ],
     {ok, InfoList}.
 
@@ -770,6 +792,72 @@ 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{meta=Meta}=Doc, Options) ->
+    case proplists:get_value(conflicts, Meta) of
+        undefined -> % no conflicts
+            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;
+        _Else -> % only admins can read conflicted docs in _access dbs
+            case is_admin(Db) of
+                true -> ok;
+                _Else2 -> throw({forbidden, <<"document is in conflict">>})
+            end
+    end.
+validate_access2(Db, Doc) ->
+    validate_access3(check_access(Db, Doc)).
+
+validate_access3(true) -> ok;
+validate_access3(_) -> throw({forbidden, <<"can't touch this">>}).
+
+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
+        % _access = ["a", "b", ]
+        case is_admin(Db) of
+        true ->
+            true;
+        _ ->
+            case {check_name(UserName, Access), check_roles(UserRoles, Access)} of
+            {true, _} -> true;
+            {_, true} -> true;
+            _ -> false
+            end
+        end
+    end.
+
+check_name(null, _Access) -> true;
+check_name(UserName, Access) ->
+            lists:member(UserName, Access).
+% nicked from couch_db:check_security
+
+check_roles(Roles, Access) ->
+    UserRolesSet = ordsets:from_list(Roles),
+    RolesSet = ordsets:from_list(Access ++ ["_users"]),
+    not ordsets:is_disjoint(UserRolesSet, RolesSet).
+
 get_admins(#db{security = SecProps}) ->
     couch_util:get_value(<<"admins">>, SecProps, {[]}).
 
@@ -911,9 +999,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 ->
+           case catch check_is_admin(Db) of
+               ok -> validate_ddoc(Db, Doc);
+               Error -> Error
+           end
     end;
 validate_doc_update(#db{validate_doc_funs = undefined} = Db, Doc, Fun) ->
     ValidationFuns = load_validation_funs(Db),
@@ -1308,6 +1401,32 @@ doc_tag(#doc{meta = Meta}) ->
         Else -> throw({invalid_doc_tag, Else})
     end.
 
+validate_update(Db, Doc) ->
+    case catch validate_access(Db, Doc) of
+        ok -> Doc;
+        Error -> Error
+    end.
+
+
+validate_docs_access(Db, DocBuckets, DocErrors) ->
+            validate_docs_access1(Db, DocBuckets, {[], DocErrors}).
+
+validate_docs_access1(_Db, [], {DocBuckets0, DocErrors}) ->
+            DocBuckets1 = lists:reverse(lists:map(fun lists:reverse/1, DocBuckets0)),
+    DocBuckets = case DocBuckets1 of
+        [[]] -> [];
+        Else -> Else
+    end,
+    {ok, DocBuckets, lists:reverse(DocErrors)};
+validate_docs_access1(Db, [DocBucket|RestBuckets], {DocAcc, ErrorAcc}) ->
+    {NewBuckets, NewErrors} = lists:foldl(fun(Doc, {Acc, ErrAcc}) ->
+        case catch validate_access(Db, Doc) of
+            ok -> {[Doc|Acc], ErrAcc};
+            Error -> {Acc, [{doc_tag(Doc), Error}|ErrAcc]}
+        end
+    end, {[], ErrorAcc}, DocBucket),
+    validate_docs_access1(Db, RestBuckets, {[NewBuckets | DocAcc], NewErrors}).
+
 update_docs(Db, Docs0, Options, replicated_changes) ->
     Docs = tag_docs(Docs0),
 
@@ -1331,13 +1450,35 @@ update_docs(Db, Docs0, Options, replicated_changes) ->
         ]
      || Bucket <- DocBuckets
     ],
-    {ok, _} = write_and_commit(
+    {ok, Results} = write_and_commit(
         Db,
         DocBuckets2,
         NonRepDocs,
         [merge_conflicts | 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};
+               _ ->
+                   % dig out FDIs from Docs matching our tags/refs
+                   DocsDict = lists:foldl(fun(Doc, Dict) ->
+                       Tag = doc_tag(Doc),
+                       dict:store(Tag, Doc, Dict)
+                   end, dict:new(), Docs),
+                   AccessResults = lists:map(fun({Ref, Access}) ->
+                       { dict:fetch(Ref, DocsDict), Access }
+                   end, AccessViolations),
+                   {ok, AccessResults}
+           end
+   end;
+
 update_docs(Db, Docs0, Options, interactive_edit) ->
     Docs = tag_docs(Docs0),
 
@@ -1459,7 +1600,7 @@ write_and_commit(
     MergeConflicts = lists:member(merge_conflicts, Options),
     MRef = erlang:monitor(process, Pid),
     try
-        Pid ! {update_docs, self(), DocBuckets, NonRepDocs, MergeConflicts},
+        Pid ! {update_docs, self(), DocBuckets, NonRepDocs, MergeConflicts, Ctx},
         case collect_results_with_metrics(Pid, MRef, []) of
             {ok, Results} ->
                 {ok, Results};
@@ -1474,7 +1615,7 @@ write_and_commit(
                 % We only retry once
                 DocBuckets3 = prepare_doc_summaries(Db2, DocBuckets2),
                 close(Db2),
-                Pid ! {update_docs, self(), DocBuckets3, NonRepDocs, MergeConflicts},
+                Pid ! {update_docs, self(), DocBuckets3, NonRepDocs, MergeConflicts, Ctx},
                 case collect_results_with_metrics(Pid, MRef, []) of
                     {ok, Results} -> {ok, Results};
                     retry -> throw({update_error, compaction_retry})
@@ -1686,6 +1827,12 @@ open_read_stream(Db, AttState) ->
 is_active_stream(Db, StreamEngine) ->
     couch_db_engine:is_active_stream(Db, StreamEngine).
 
+changes_since(Db, StartSeq, Fun, Options, Acc) when is_record(Db, db) ->
+    case couch_db:has_access_enabled(Db) and not couch_db:is_admin(Db) of
+        true -> couch_mrview:query_changes_access(Db, StartSeq, Fun, Options, Acc);
+        false -> couch_db_engine:fold_changes(Db, StartSeq, Fun, Options, Acc)
+    end.
+
 calculate_start_seq(_Db, _Node, Seq) when is_integer(Seq) ->
     Seq;
 calculate_start_seq(Db, Node, {Seq, Uuid}) ->
@@ -1814,7 +1961,10 @@ fold_changes(Db, StartSeq, UserFun, UserAcc) ->
     fold_changes(Db, StartSeq, UserFun, UserAcc, []).
 
 fold_changes(Db, StartSeq, UserFun, UserAcc, Opts) ->
-    couch_db_engine:fold_changes(Db, StartSeq, UserFun, UserAcc, Opts).
+    case couch_db:has_access_enabled(Db) and not couch_db:is_admin(Db) of
+        true -> couch_mrview:query_changes_access(Db, StartSeq, UserFun, Opts, UserAcc);
+        false -> couch_db_engine:fold_changes(Db, StartSeq, UserFun, UserAcc, Opts)
+    end.
 
 fold_purge_infos(Db, StartPurgeSeq, Fun, Acc) ->
     fold_purge_infos(Db, StartPurgeSeq, Fun, Acc, []).
@@ -1832,7 +1982,7 @@ open_doc_revs_int(Db, IdRevs, Options) ->
     lists:zipwith(
         fun({Id, Revs}, Lookup) ->
             case Lookup of
-                #full_doc_info{rev_tree = RevTree} ->
+                #full_doc_info{rev_tree = RevTree, access = Access} ->
                     {FoundRevs, MissingRevs} =
                         case Revs of
                             all ->
@@ -1853,7 +2003,7 @@ open_doc_revs_int(Db, IdRevs, Options) ->
                                         % we have the rev in our list but know nothing about it
                                         {{not_found, missing}, {Pos, Rev}};
                                     #leaf{deleted = IsDeleted, ptr = SummaryPtr} ->
-                                        {ok, make_doc(Db, Id, IsDeleted, SummaryPtr, FoundRevPath)}
+                                        {ok, make_doc(Db, Id, IsDeleted, SummaryPtr, FoundRevPath, Access)}
                                 end
                             end,
                             FoundRevs
@@ -1875,23 +2025,29 @@ open_doc_revs_int(Db, IdRevs, Options) ->
 open_doc_int(Db, <<?LOCAL_DOC_PREFIX, _/binary>> = Id, Options) ->
     case couch_db_engine:open_local_docs(Db, [Id]) of
         [#doc{} = Doc] ->
-            apply_open_options({ok, Doc}, Options);
+        case Doc#doc.body of
+            { Body } ->
+                Access = couch_util:get_value(<<"_access">>, Body),
+                apply_open_options(Db, {ok, Doc#doc{access = Access}}, Options);
+            _Else ->
+                apply_open_options(Db, {ok, Doc}, Options)
+        end;
         [not_found] ->
             {not_found, missing}
     end;
-open_doc_int(Db, #doc_info{id = Id, revs = [RevInfo | _]} = DocInfo, Options) ->
+open_doc_int(Db, #doc_info{id = Id, revs = [RevInfo | _], access = Access} = DocInfo, Options) ->
     #rev_info{deleted = IsDeleted, rev = {Pos, RevId}, body_sp = Bp} = RevInfo,
-    Doc = make_doc(Db, Id, IsDeleted, Bp, {Pos, [RevId]}),
+    Doc = make_doc(Db, Id, IsDeleted, Bp, {Pos, [RevId], Access}),
     apply_open_options(
-        {ok, Doc#doc{meta = doc_meta_info(DocInfo, [], Options)}}, Options
+        {ok, Doc#doc{meta = doc_meta_info(DocInfo, [], Options)}}, Options, Access
     );
-open_doc_int(Db, #full_doc_info{id = Id, rev_tree = RevTree} = FullDocInfo, Options) ->
+open_doc_int(Db, #full_doc_info{id = Id, rev_tree = RevTree, access = Access} = FullDocInfo, Options) ->
     #doc_info{revs = [#rev_info{deleted = IsDeleted, rev = Rev, body_sp = Bp} | _]} =
         DocInfo = couch_doc:to_doc_info(FullDocInfo),
     {[{_, RevPath}], []} = couch_key_tree:get(RevTree, [Rev]),
-    Doc = make_doc(Db, Id, IsDeleted, Bp, RevPath),
+    Doc = make_doc(Db, Id, IsDeleted, Bp, RevPath, Access),
     apply_open_options(
-        {ok, Doc#doc{meta = doc_meta_info(DocInfo, RevTree, Options)}}, Options
+        {ok, Doc#doc{meta = doc_meta_info(DocInfo, RevTree, Options)}}, Options, Access
     );
 open_doc_int(Db, Id, Options) ->
     case get_full_doc_info(Db, Id) of
@@ -1952,21 +2108,26 @@ doc_meta_info(
             true -> [{local_seq, Seq}]
         end.
 
-make_doc(_Db, Id, Deleted, nil = _Bp, RevisionPath) ->
+make_doc(Db, Id, Deleted, Bp, {Pos, Revs}) ->
+    make_doc(Db, Id, Deleted, Bp, {Pos, Revs}, []).
+
+make_doc(_Db, Id, Deleted, nil = _Bp, RevisionPath, Access) ->
     #doc{
         id = Id,
         revs = RevisionPath,
         body = [],
         atts = [],
-        deleted = Deleted
+        deleted = Deleted,
+        access = Access
     };
-make_doc(#db{} = Db, Id, Deleted, Bp, {Pos, Revs}) ->
+make_doc(#db{} = Db, Id, Deleted, Bp, {Pos, Revs}, Access) ->
     RevsLimit = get_revs_limit(Db),
     Doc0 = couch_db_engine:read_doc_body(Db, #doc{
         id = Id,
         revs = {Pos, lists:sublist(Revs, 1, RevsLimit)},
         body = Bp,
-        deleted = Deleted
+        deleted = Deleted,
+        access = Access
     }),
     Doc1 =
         case Doc0#doc.atts of
diff --git a/src/couch/src/couch_db_int.hrl b/src/couch/src/couch_db_int.hrl
index 7da0ce5df..b67686fab 100644
--- a/src/couch/src/couch_db_int.hrl
+++ b/src/couch/src/couch_db_int.hrl
@@ -37,7 +37,8 @@
     waiting_delayed_commit_deprecated,
 
     options = [],
-    compression
+    compression,
+    access = false
 }).
 
 
diff --git a/src/couch/src/couch_db_updater.erl b/src/couch/src/couch_db_updater.erl
index 17a1e9160..e61934e51 100644
--- a/src/couch/src/couch_db_updater.erl
+++ b/src/couch/src/couch_db_updater.erl
@@ -24,6 +24,11 @@
 % 10 GiB
 -define(DEFAULT_MAX_PARTITION_SIZE, 16#280000000).
 
+-define(DEFAULT_SECURITY_OBJECT, [
+    {<<"members">>,{[{<<"roles">>,[<<"_admin">>]}]}},
+    {<<"admins">>, {[{<<"roles">>,[<<"_admin">>]}]}}
+]).
+
 -record(merge_acc, {
     revs_limit,
     merge_conflicts,
@@ -36,7 +41,7 @@
 init({Engine, DbName, FilePath, Options0}) ->
     erlang:put(io_priority, {db_update, DbName}),
     update_idle_limit_from_config(),
-    DefaultSecObj = default_security_object(DbName),
+    DefaultSecObj = default_security_object(DbName, Options0),
     Options = [{default_security_object, DefaultSecObj} | Options0],
     try
         {ok, EngineState} = couch_db_engine:init(Engine, FilePath, Options),
@@ -165,7 +170,7 @@ handle_cast(Msg, #db{name = Name} = Db) ->
     {stop, Msg, Db}.
 
 handle_info(
-    {update_docs, Client, GroupedDocs, NonRepDocs, MergeConflicts},
+    {update_docs, Client, GroupedDocs, NonRepDocs, MergeConflicts, UserCtx},
     Db
 ) ->
     GroupedDocs2 = sort_and_tag_grouped_docs(Client, GroupedDocs),
@@ -181,7 +186,7 @@ handle_info(
             Clients = [Client]
     end,
     NonRepDocs2 = [{Client, NRDoc} || NRDoc <- NonRepDocs],
-    try update_docs_int(Db, GroupedDocs3, NonRepDocs2, MergeConflicts) of
+    try update_docs_int(Db, GroupedDocs3, NonRepDocs2, MergeConflicts, UserCtx) of
         {ok, Db2, UpdatedDDocIds} ->
             ok = couch_server:db_updated(Db2),
             case {couch_db:get_update_seq(Db), couch_db:get_update_seq(Db2)} of
@@ -260,7 +265,11 @@ sort_and_tag_grouped_docs(Client, GroupedDocs) ->
     % The merge_updates function will fail and the database can end up with
     % duplicate documents if the incoming groups are not sorted, so as a sanity
     % check we sort them again here. See COUCHDB-2735.
-    Cmp = fun([#doc{id = A} | _], [#doc{id = B} | _]) -> A < B end,
+    Cmp = fun
+        ([], []) -> false; % TODO: re-evaluate this addition, might be
+                           %       superflous now
+        ([#doc{id=A}|_], [#doc{id=B}|_]) -> A < B
+     end,
     lists:map(
         fun(DocGroup) ->
             [{Client, maybe_tag_doc(D)} || D <- DocGroup]
@@ -320,6 +329,7 @@ init_db(DbName, FilePath, EngineState, Options) ->
     BDU = couch_util:get_value(before_doc_update, Options, nil),
     ADR = couch_util:get_value(after_doc_read, Options, nil),
 
+    Access = couch_util:get_value(access, Options, false),
     NonCreateOpts = [Opt || Opt <- Options, Opt /= create],
 
     InitDb = #db{
@@ -329,7 +339,8 @@ init_db(DbName, FilePath, EngineState, Options) ->
         instance_start_time = StartTime,
         options = NonCreateOpts,
         before_doc_update = BDU,
-        after_doc_read = ADR
+        after_doc_read = ADR,
+        access = Access
     },
 
     DbProps = couch_db_engine:get_props(InitDb),
@@ -390,7 +401,8 @@ flush_trees(
                             active = WrittenSize,
                             external = ExternalSize
                         },
-                        atts = AttSizeInfo
+                        atts = AttSizeInfo,
+                        access = NewDoc#doc.access
                     },
                     {Leaf, add_sizes(Type, Leaf, SizesAcc)};
                 #leaf{} ->
@@ -475,6 +487,9 @@ doc_tag(#doc{meta = Meta}) ->
         Else -> throw({invalid_doc_tag, Else})
     end.
 
+merge_rev_trees([[]], [], Acc) ->
+    % validate_docs_access left us with no docs to merge
+    {ok, Acc};
 merge_rev_trees([], [], Acc) ->
     {ok, Acc#merge_acc{
         add_infos = lists:reverse(Acc#merge_acc.add_infos)
@@ -656,22 +671,29 @@ maybe_stem_full_doc_info(#full_doc_info{rev_tree = Tree} = Info, Limit) ->
             Info
     end.
 
-update_docs_int(Db, DocsList, LocalDocs, MergeConflicts) ->
+update_docs_int(Db, DocsList, LocalDocs, MergeConflicts, UserCtx) ->
     UpdateSeq = couch_db_engine:get_update_seq(Db),
     RevsLimit = couch_db_engine:get_revs_limit(Db),
 
-    Ids = [Id || [{_Client, #doc{id = Id}} | _] <- DocsList],
+    Ids = [Id || [{_Client, #doc{id=Id}}|_] <- DocsList],
+    % TODO: maybe a perf hit, instead of zip3-ing existing Accesses into
+    %       our doc lists, maybe find 404 docs differently down in
+    %       validate_docs_access (revs is [], which we can then use
+    %       to skip validation as we know it is the first doc rev)
+    Accesses = [Access || [{_Client, #doc{access=Access}}|_] <- DocsList],
+
     % lookup up the old documents, if they exist.
     OldDocLookups = couch_db_engine:open_docs(Db, Ids),
-    OldDocInfos = lists:zipwith(
+    OldDocInfos = lists:zipwith3(
         fun
-            (_Id, #full_doc_info{} = FDI) ->
+            (_Id, #full_doc_info{} = FDI, _Access) ->
                 FDI;
-            (Id, not_found) ->
-                #full_doc_info{id = Id}
+            (Id, not_found, Access) ->
+               #full_doc_info{id=Id,access=Access}
         end,
         Ids,
-        OldDocLookups
+        OldDocLookups,
+        Accesses
     ),
 
     %% Get the list of full partitions
@@ -708,7 +730,14 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts) ->
         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, UserCtx, DocsList, OldDocInfos),
+    {ok, AccOut} = merge_rev_trees(DocsListValidated, OldDocInfosValidated, AccIn),
     #merge_acc{
         add_infos = NewFullDocInfos,
         rem_seqs = RemSeqs
@@ -718,7 +747,8 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts) ->
     % 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(Db, LocalDocs),
+    LocalDocs2 = update_local_doc_revs(LocalDocs1),
 
     {ok, Db1} = couch_db_engine:write_doc_infos(Db, Pairs, LocalDocs2),
 
@@ -733,18 +763,87 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts) ->
         length(LocalDocs2)
     ),
 
-    % Check if we just updated any design documents, and update the validation
-    % funs if we did.
+    % Check if we just updated any non-access design documents,
+    % and update the validation funs if we did.
+    NonAccessIds = [Id || [{_Client, #doc{id=Id,access=[]}}|_] <- DocsList],
     UpdatedDDocIds = lists:flatmap(
         fun
             (<<"_design/", _/binary>> = Id) -> [Id];
             (_) -> []
         end,
-        Ids
+        NonAccessIds
     ),
 
     {ok, commit_data(Db1), UpdatedDDocIds}.
 
+% check_access(Db, UserCtx, Access) ->
+%     check_access(Db, UserCtx, couch_db:has_access_enabled(Db), Access).
+%
+% check_access(_Db, UserCtx, false, _Access) ->
+%     true;
+
+% 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).
+
+% TODO: looks like we go into validation here unconditionally and only check in
+%       check_access() whether the Db has_access_enabled(), we should do this
+%       here on the outside. Might be our perf issue.
+%       However, if it is, that means we have to speed this up as it would still
+%       be too slow for when access is enabled.
+validate_docs_access(Db, UserCtx, DocsList, OldDocInfos) ->
+    case couch_db:has_access_enabled(Db) of
+        true -> validate_docs_access_int(Db, UserCtx, DocsList, OldDocInfos);
+        _Else -> { DocsList, OldDocInfos }
+    end.
+
+validate_docs_access_int(Db, UserCtx, DocsList, OldDocInfos) ->
+    validate_docs_access(Db, UserCtx, DocsList, OldDocInfos, [], []).
+
+validate_docs_access(_Db, UserCtx, [], [], DocsListValidated, OldDocInfosValidated) ->
+    { lists:reverse(DocsListValidated), lists:reverse(OldDocInfosValidated) };
+validate_docs_access(Db, UserCtx, [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 }, 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
+            true -> % if valid, then send to DocsListValidated, OldDocsInfo
+                    % and store the access context on the new doc
+                [{Client, Doc} | Acc];
+            _Else2 -> % if invalid, then send_result tagged `access`(c.f. `conflict)
+                      % and don’t add to DLV, nor ODI
+                send_result(Client, Doc, access),
+                Acc
+        end
+    end, [], Docs),
+
+    { NewDocsListValidated, NewOldDocInfosValidated } = case length(NewDocs) of
+        0 -> % we sent out all docs as invalid access, drop the old doc info associated with it
+            { [NewDocs | DocsListValidated], OldDocInfosValidated };
+        _ ->
+            { [NewDocs | DocsListValidated], [OldInfo | OldDocInfosValidated] }
+    end,
+    validate_docs_access(Db, UserCtx, DocRest, OldInfoRest, NewDocsListValidated, NewOldDocInfosValidated).
+
+apply_local_docs_access(Db, Docs) ->
+    apply_local_docs_access1(couch_db:has_access_enabled(Db), Docs).
+
+apply_local_docs_access1(false, Docs) ->
+    Docs;
+apply_local_docs_access1(true, 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) ->
@@ -761,6 +860,14 @@ update_local_doc_revs(Docs) ->
         Docs
     ).
 
+default_security_object(DbName, []) ->
+    default_security_object(DbName);
+default_security_object(DbName, Options) ->
+    case lists:member({access, true}, Options) of
+        false -> default_security_object(DbName);
+        true -> ?DEFAULT_SECURITY_OBJECT
+    end.
+
 increment_local_doc_revs(#doc{deleted = true} = Doc) ->
     {ok, Doc#doc{revs = {0, [0]}}};
 increment_local_doc_revs(#doc{revs = {0, []}} = Doc) ->
@@ -925,21 +1032,14 @@ get_meta_body_size(Meta) ->
 
 default_security_object(<<"shards/", _/binary>>) ->
     case config:get("couchdb", "default_security", "admin_only") of
-        "admin_only" ->
-            [
-                {<<"members">>, {[{<<"roles">>, [<<"_admin">>]}]}},
-                {<<"admins">>, {[{<<"roles">>, [<<"_admin">>]}]}}
-            ];
+        "admin_only" -> ?DEFAULT_SECURITY_OBJECT;
         Everyone when Everyone == "everyone"; Everyone == "admin_local" ->
             []
     end;
 default_security_object(_DbName) ->
     case config:get("couchdb", "default_security", "admin_only") of
         Admin when Admin == "admin_only"; Admin == "admin_local" ->
-            [
-                {<<"members">>, {[{<<"roles">>, [<<"_admin">>]}]}},
-                {<<"admins">>, {[{<<"roles">>, [<<"_admin">>]}]}}
-            ];
+           ?DEFAULT_SECURITY_OBJECT;
         "everyone" ->
             []
     end.


[couchdb] 06/08: feat(access): expand couch_btree / bt_engine to handle access

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit a6f3f38a5499d99da4b41f425918b9ab0da9fecb
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 17:28:12 2022 +0200

    feat(access): expand couch_btree / bt_engine to handle access
---
 src/couch/src/couch_bt_engine.erl | 27 +++++++++++++++++----------
 src/couch/src/couch_btree.erl     | 12 ++++++++++++
 2 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/src/couch/src/couch_bt_engine.erl b/src/couch/src/couch_bt_engine.erl
index 486ed7cb0..87154e9ef 100644
--- a/src/couch/src/couch_bt_engine.erl
+++ b/src/couch/src/couch_bt_engine.erl
@@ -664,20 +664,22 @@ id_tree_split(#full_doc_info{} = Info) ->
         update_seq = Seq,
         deleted = Deleted,
         sizes = SizeInfo,
-        rev_tree = Tree
+        rev_tree = Tree,
+        access = Access
     } = Info,
-    {Id, {Seq, ?b2i(Deleted), split_sizes(SizeInfo), disk_tree(Tree)}}.
+    {Id, {Seq, ?b2i(Deleted), split_sizes(SizeInfo), disk_tree(Tree), split_access(Access)}}.
 
 id_tree_join(Id, {HighSeq, Deleted, DiskTree}) ->
     % Handle old formats before data_size was added
-    id_tree_join(Id, {HighSeq, Deleted, #size_info{}, DiskTree});
-id_tree_join(Id, {HighSeq, Deleted, Sizes, DiskTree}) ->
+    id_tree_join(Id, {HighSeq, Deleted, #size_info{}, DiskTree, []});
+id_tree_join(Id, {HighSeq, Deleted, Sizes, DiskTree, Access}) ->
     #full_doc_info{
         id = Id,
         update_seq = HighSeq,
         deleted = ?i2b(Deleted),
         sizes = couch_db_updater:upgrade_sizes(Sizes),
-        rev_tree = rev_tree(DiskTree)
+        rev_tree = rev_tree(DiskTree),
+        access = join_access(Access)
     }.
 
 id_tree_reduce(reduce, FullDocInfos) ->
@@ -714,19 +716,21 @@ seq_tree_split(#full_doc_info{} = Info) ->
         update_seq = Seq,
         deleted = Del,
         sizes = SizeInfo,
-        rev_tree = Tree
+        rev_tree = Tree,
+        access = Access
     } = Info,
-    {Seq, {Id, ?b2i(Del), split_sizes(SizeInfo), disk_tree(Tree)}}.
+    {Seq, {Id, ?b2i(Del), split_sizes(SizeInfo), disk_tree(Tree), split_access(Access)}}.
 
 seq_tree_join(Seq, {Id, Del, DiskTree}) when is_integer(Del) ->
-    seq_tree_join(Seq, {Id, Del, {0, 0}, DiskTree});
-seq_tree_join(Seq, {Id, Del, Sizes, DiskTree}) when is_integer(Del) ->
+    seq_tree_join(Seq, {Id, Del, {0, 0}, DiskTree, []});
+seq_tree_join(Seq, {Id, Del, Sizes, DiskTree, Access}) when is_integer(Del) ->
     #full_doc_info{
         id = Id,
         update_seq = Seq,
         deleted = ?i2b(Del),
         sizes = join_sizes(Sizes),
-        rev_tree = rev_tree(DiskTree)
+        rev_tree = rev_tree(DiskTree),
+        access = join_access(Access)
     };
 seq_tree_join(KeySeq, {Id, RevInfos, DeletedRevInfos}) ->
     % Older versions stored #doc_info records in the seq_tree.
@@ -755,6 +759,9 @@ seq_tree_reduce(reduce, DocInfos) ->
 seq_tree_reduce(rereduce, Reds) ->
     lists:sum(Reds).
 
+join_access(Access) -> Access.
+split_access(Access) -> Access.
+
 local_tree_split(#doc{revs = {0, [Rev]}} = Doc) when is_binary(Rev) ->
     #doc{
         id = Id,
diff --git a/src/couch/src/couch_btree.erl b/src/couch/src/couch_btree.erl
index b974a22ee..d7ca7bab4 100644
--- a/src/couch/src/couch_btree.erl
+++ b/src/couch/src/couch_btree.erl
@@ -16,6 +16,7 @@
 -export([fold/4, full_reduce/1, final_reduce/2, size/1, foldl/3, foldl/4]).
 -export([fold_reduce/4, lookup/2, get_state/1, set_options/2]).
 -export([extract/2, assemble/3, less/3]).
+-export([full_reduce_with_options/2]).
 
 -include_lib("couch/include/couch_db.hrl").
 
@@ -109,6 +110,17 @@ full_reduce(#btree{root = nil, reduce = Reduce}) ->
 full_reduce(#btree{root = Root}) ->
     {ok, element(2, Root)}.
 
+full_reduce_with_options(Bt, Options0) ->
+    CountFun = fun(_SeqStart, PartialReds, 0) ->
+        {ok, couch_btree:final_reduce(Bt, PartialReds)}
+    end,
+    [UserName] = proplists:get_value(start_key, Options0, <<"">>),
+    EndKey = {[UserName, {[]}]},
+    Options = Options0 ++ [
+        {end_key, EndKey}
+    ],
+    fold_reduce(Bt, CountFun, 0, Options).
+
 size(#btree{root = nil}) ->
     0;
 size(#btree{root = {_P, _Red}}) ->


[couchdb] 01/08: feat(access): add access handling to chttpd

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jan pushed a commit to branch feat/access-2022
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 3a2a9c611fda07f24837fa9a6a85e72a20cdcb8a
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Fri Jun 24 15:24:22 2022 +0200

    feat(access): add access handling to chttpd
---
 src/chttpd/src/chttpd.erl      |  2 ++
 src/chttpd/src/chttpd_db.erl   | 21 ++++++++++++++++-----
 src/chttpd/src/chttpd_view.erl | 15 +++++++++++++++
 3 files changed, 33 insertions(+), 5 deletions(-)

diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 93b610719..51922c8d9 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -1031,6 +1031,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) ->
+    {403, <<"forbidden">>, <<"access">>};
 error_info(database_does_not_exist) ->
     {404, <<"not_found">>, <<"Database does not exist.">>};
 error_info(not_found) ->
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 4392df194..92affa4ec 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -1037,16 +1037,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"),
+    case Revs of
         undefined ->
             Body = {[{<<"_deleted">>, true}]};
         Rev ->
             Body = {[{<<"_rev">>, ?l2b(Rev)}, {<<"_deleted">>, true}]}
     end,
-    Doc = couch_doc_from_req(Req, Db, DocId, Body),
-    send_updated_doc(Req, Db, DocId, Doc);
+    Doc = Doc0#doc{revs=Revs,body=Body,deleted=true},
+    send_updated_doc(Req, Db, DocId, couch_doc_from_req(Req, Db, DocId, Doc));
 db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) ->
     #doc_query_args{
         rev = Rev0,
@@ -1492,6 +1494,8 @@ receive_request_data(Req, LenLeft) when LenLeft > 0 ->
 receive_request_data(_Req, _) ->
     throw(<<"expected more data">>).
 
+update_doc_result_to_json({#doc{id=Id,revs=Rev}, access}) ->
+    update_doc_result_to_json({{Id, Rev}, access});
 update_doc_result_to_json({error, _} = Error) ->
     {_Code, Err, Msg} = chttpd:error_info(Error),
     {[
@@ -2046,6 +2050,7 @@ parse_shards_opt(Req) ->
     [
         {n, parse_shards_opt("n", Req, config:get_integer("cluster", "n", 3))},
         {q, parse_shards_opt("q", Req, config:get_integer("cluster", "q", 2))},
+        {access, parse_shards_opt_access(chttpd:qs_value(Req, "access", false))},
         {placement,
             parse_shards_opt(
                 "placement", Req, config:get("cluster", "placement")
@@ -2082,6 +2087,12 @@ parse_shards_opt(Param, Req, Default) ->
         false -> throw({bad_request, Err})
     end.
 
+parse_shards_opt_access(Value) when is_boolean(Value) ->
+    Value;
+parse_shards_opt_access(_Value) ->
+    Err = ?l2b(["The `access` value should be a boolean."]),
+    throw({bad_request, Err}).
+
 parse_engine_opt(Req) ->
     case chttpd:qs_value(Req, "engine") of
         undefined ->
diff --git a/src/chttpd/src/chttpd_view.erl b/src/chttpd/src/chttpd_view.erl
index 1d721d189..f74088dbc 100644
--- a/src/chttpd/src/chttpd_view.erl
+++ b/src/chttpd/src/chttpd_view.erl
@@ -69,6 +69,21 @@ fabric_query_view(Db, Req, DDoc, ViewName, Args) ->
     Max = chttpd:chunked_response_buffer_size(),
     VAcc = #vacc{db = Db, req = Req, threshold = Max},
     Options = [{user_ctx, Req#httpd.user_ctx}],
+%    {ok, Resp} = fabric:query_view(Db, Options, DDoc, ViewName,
+%            fun view_cb/2, VAcc, Args),
+%    {ok, Resp#vacc.resp}.
+%    % TODO: This might just be a debugging leftover, we might be able
+%    %       to undo this by just returning {ok, Resp#vacc.resp}
+%    %       However, this *might* be here because we need to handle
+%    %       errors here now, because access might tell us to.
+%    case fabric:query_view(Db, Options, DDoc, ViewName,
+%            fun view_cb/2, VAcc, Args) of
+%        {ok, Resp} ->
+%            {ok, Resp#vacc.resp};
+%        {error, Error} ->
+%            throw(Error)
+%    end.
+
     {ok, Resp} = fabric:query_view(
         Db,
         Options,