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,