Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP, but please review]: per-doc-access-control #4139

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9bc4dda
feat(access): add access handling to chttpd
janl Jun 24, 2022
ea4e3cc
feat(access): add access to couch_db internal records
janl Jun 24, 2022
d95945c
feat(access): handle new records in couch_doc
janl Jun 24, 2022
a9eb33d
feat(access): add new _users role for all authenticated users
janl Jun 24, 2022
78eb8d8
feat(access): add access query server
janl Jun 24, 2022
cc05b2b
feat(access): expand couch_btree / bt_engine to handle access
janl Jun 24, 2022
ae30074
feat(access): handle access in couch_db[_updater]
janl Jun 24, 2022
1d7162e
feat(access): add util functions
janl Jun 25, 2022
8c3e295
feat(access): adjust existing tests
janl Jun 25, 2022
9d3fe31
feat(access): add mrview machinery
janl Jun 25, 2022
9af77d9
feat(access): add access tests
janl Jun 25, 2022
bcd3cce
feat(access): add access handling to replicator
janl Jun 27, 2022
4680045
feat(access): add access handling to ddoc cache
janl Jun 27, 2022
92944c3
feat(access): add access handling to fabric
janl Jun 27, 2022
865e428
feat(access): additional test fixes
janl Jun 27, 2022
886ab2f
fix: make tests pass again
janl Jul 23, 2022
651df0a
feat(access): add global off switch
janl Aug 6, 2022
e1746c5
doc(access): leave todo for missing implementation detail
janl Aug 6, 2022
2084d51
chore(access): remove old comment
janl Aug 6, 2022
3c5bdf4
fix(access): use minimal info from prev rev
janl Aug 6, 2022
fa8585c
chore(access): style notes
janl Aug 6, 2022
92f36af
doc(access): add todos
janl Aug 6, 2022
b6a7521
fix(access): opt-out switch
janl Aug 6, 2022
a443c03
test(access): test disable access config
janl Aug 6, 2022
3776dca
fix(access): elixir tests
janl Aug 6, 2022
d6863a7
chore(access): erlfmt
janl Aug 6, 2022
6d437e3
chore: remove comments and stale todo entries
janl Aug 20, 2022
691343f
fix(access) elixir tests again
janl Aug 20, 2022
5b9e274
fix: simplify
janl Aug 20, 2022
207cdb3
chore: append _users role instead of prepending it
janl Nov 11, 2022
9db67a9
fix: restore previous function signature
janl Nov 11, 2022
841bc38
fix: add function signature change to new open_docs_rev/3
janl Nov 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions rel/overlay/etc/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ authentication_db = _users
; max_iterations, password_scheme, password_regexp, proxy_use_secret,
; public_fields, secret, users_db_public, cookie_domain, same_site

; Per document access settings
[per_doc_access]
;enabled = false

; CSP (Content Security Policy) Support
[csp]
;utils_enable = true
Expand Down
2 changes: 2 additions & 0 deletions src/chttpd/src/chttpd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
31 changes: 26 additions & 5 deletions src/chttpd/src/chttpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -955,16 +955,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}]),
Copy link
Member Author

@janl janl Aug 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original Comment:

If this fails (due to access restrictions) how does the 403 bubble back up to the user? I followed chttpd_db:couch_doc_open/4 to fabric:open_doc/3 to fabric_doc_open:go/3 through to couch_db:get_doc_info/2 but I couldn't work out where the access restriction is enforced.

I presume we end up at some point in couch_db:validate_access or check_access which throws a {forbidden, "something"} but I couldn't see how this translates into a 403.

#3038 (comment)

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 = #doc{revs = Revs, body = Body, deleted = true, access = Doc0#doc.access},
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,
Expand Down Expand Up @@ -1414,6 +1416,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),
{[
Expand Down Expand Up @@ -1968,6 +1972,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", Req, chttpd:qs_value(Req, "access", false))},
{placement,
parse_shards_opt(
"placement", Req, config:get("cluster", "placement")
Expand Down Expand Up @@ -1996,7 +2001,23 @@ parse_shards_opt("placement", Req, Default) ->
throw({bad_request, Err})
end
end;
parse_shards_opt("access", Req, Value) when is_list(Value) ->
parse_shards_opt("access", Req, list_to_existing_atom(Value));
parse_shards_opt("access", _Req, Value) when Value =:= true ->
case config:get_boolean("per_doc_access", "enabled", false) of
true ->
true;
false ->
Err = ?l2b(["The `access` option is not available on this CouchDB installation."]),
throw({bad_request, Err})
end;
parse_shards_opt("access", _Req, Value) when Value =:= false ->
false;
parse_shards_opt("access", _Req, _Value) ->
Err = ?l2b(["The `access` value should be a boolean."]),
throw({bad_request, Err});
parse_shards_opt(Param, Req, Default) ->
couch_log:error("~n parse_shards_opt Param: ~p, Default: ~p~n", [Param, Default]),
Val = chttpd:qs_value(Req, Param, Default),
Err = ?l2b(["The `", Param, "` value should be a positive integer."]),
case couch_util:validate_positive_int(Val) of
Expand Down
1 change: 1 addition & 0 deletions src/chttpd/src/chttpd_view.erl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ 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,
Expand Down
12 changes: 8 additions & 4 deletions src/couch/include/couch_db.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
-record(doc_info, {
id = <<"">>,
high_seq = 0,
revs = [] % rev_info
revs = [], % rev_info
access = []
}).

-record(size_info, {
Expand All @@ -78,7 +79,8 @@
update_seq = 0,
deleted = false,
rev_tree = [],
sizes = #size_info{}
sizes = #size_info{},
access = []
}).

-record(httpd, {
Expand Down Expand Up @@ -122,7 +124,8 @@

% key/value tuple of meta information, provided when using special options:
% couch_db:open_doc(Db, Id, Options).
meta = []
meta = [],
access = []
}).


Expand Down Expand Up @@ -205,7 +208,8 @@
ptr,
seq,
sizes = #size_info{},
atts = []
atts = [],
access = []
}).

-record (fabric_changes_acc, {
Expand Down
139 changes: 139 additions & 0 deletions src/couch/src/couch_access_native_proc.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

-module(couch_access_native_proc).
-behavior(gen_server).

-export([
start_link/0,
set_timeout/2,
prompt/2
]).

-export([
init/1,
terminate/2,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3
]).

-record(st, {
indexes = [],
% TODO: make configurable
timeout = 5000
}).

start_link() ->
gen_server:start_link(?MODULE, [], []).

set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut > 0 ->
gen_server:call(Pid, {set_timeout, TimeOut}).

prompt(Pid, Data) ->
gen_server:call(Pid, {prompt, Data}).

init(_) ->
{ok, #st{}}.

terminate(_Reason, _St) ->
ok.

handle_call({set_timeout, TimeOut}, _From, St) ->
{reply, ok, St#st{timeout = TimeOut}};
handle_call({prompt, [<<"reset">>]}, _From, St) ->
{reply, true, St#st{indexes = []}};
handle_call({prompt, [<<"reset">>, _QueryConfig]}, _From, St) ->
{reply, true, St#st{indexes = []}};
handle_call({prompt, [<<"add_fun">>, IndexInfo]}, _From, St) ->
{reply, true, St};
handle_call({prompt, [<<"map_doc">>, Doc]}, _From, St) ->
{reply, map_doc(St, mango_json:to_binary(Doc)), St};
handle_call({prompt, [<<"reduce">>, _, _]}, _From, St) ->
{reply, null, St};
handle_call({prompt, [<<"rereduce">>, _, _]}, _From, St) ->
{reply, null, St};
handle_call({prompt, [<<"index_doc">>, Doc]}, _From, St) ->
{reply, [[]], St};
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

handle_cast(garbage_collect, St) ->
erlang:garbage_collect(),
{noreply, St};
handle_cast(Msg, St) ->
{stop, {invalid_cast, Msg}, St}.

handle_info(Msg, St) ->
{stop, {invalid_info, Msg}, St}.

code_change(_OldVsn, St, _Extra) ->
{ok, St}.

% return value is an array of arrays, first dimension is the different indexes
% [0] will be by-access-id // for this test, later we should make this by-access
% -seq, since that one we will always need, and by-access-id can be opt-in.
% the second dimension is the number of emit kv pairs:
% [ // the return value
% [ // the first view
% ['k1', 'v1'], // the first k/v pair for the first view
% ['k2', 'v2'] // second, etc.
% ],
% [ // second view
% ['l1', 'w1'] // first k/v par in second view
% ]
% ]
% {"id":"account/bongel","key":"account/bongel","value":{"rev":"1-967a00dff5e02add41819138abb3284d"}},

map_doc(_St, {Doc}) ->
case couch_util:get_value(<<"_access">>, Doc) of
undefined ->
% do not index this doc
[[], []];
Access when is_list(Access) ->
Id = couch_util:get_value(<<"_id">>, Doc),
Rev = couch_util:get_value(<<"_rev">>, Doc),
Seq = couch_util:get_value(<<"_seq">>, Doc),
Deleted = couch_util:get_value(<<"_deleted">>, Doc, false),
BodySp = couch_util:get_value(<<"_body_sp">>, Doc),
% by-access-id
ById =
case Deleted of
false ->
lists:map(
fun(UserOrRole) ->
[
[[UserOrRole, Id], Rev]
]
end,
Access
);
_True ->
[[]]
end,

% by-access-seq
BySeq = lists:map(
fun(UserOrRole) ->
[
[[UserOrRole, Seq], [{rev, Rev}, {deleted, Deleted}, {body_sp, BodySp}]]
]
end,
Access
),
ById ++ BySeq;
Else ->
% TODO: no comprende: should not be needed once we implement
% _access field validation
[[], []]
end.
28 changes: 21 additions & 7 deletions src/couch/src/couch_bt_engine.erl
Original file line number Diff line number Diff line change
Expand Up @@ -664,20 +664,24 @@ 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, Sizes, 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) ->
Expand Down Expand Up @@ -714,21 +718,27 @@ 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, Sizes, 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}) ->
seq_tree_join(KeySeq, {Id, RevInfos, DeletedRevInfos, []});
seq_tree_join(KeySeq, {Id, RevInfos, DeletedRevInfos, Access}) ->
% Older versions stored #doc_info records in the seq_tree.
% Compact to upgrade.
Revs = lists:map(
Expand All @@ -746,7 +756,8 @@ seq_tree_join(KeySeq, {Id, RevInfos, DeletedRevInfos}) ->
#doc_info{
id = Id,
high_seq = KeySeq,
revs = Revs ++ DeletedRevs
revs = Revs ++ DeletedRevs,
access = Access
}.

seq_tree_reduce(reduce, DocInfos) ->
Expand All @@ -755,6 +766,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,
Expand Down
14 changes: 14 additions & 0 deletions src/couch/src/couch_btree.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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").

Expand Down Expand Up @@ -109,6 +110,19 @@ 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}}) ->
Expand Down
3 changes: 3 additions & 0 deletions src/couch/src/couch_changes.erl
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,13 @@ maybe_get_changes_doc(_Value, _Acc) ->
[].

load_doc(Db, Value, Opts, DocOpts, Filter) ->
%couch_log:error("~ncouch_changes:load_doc(): Value: ~p~n", [Value]),
case couch_index_util:load_doc(Db, Value, Opts) of
null ->
%couch_log:error("~ncouch_changes:load_doc(): null~n", []),
[{doc, null}];
Doc ->
%couch_log:error("~ncouch_changes:load_doc(): Doc: ~p~n", [Doc]),
[{doc, doc_to_json(Doc, DocOpts, Filter)}]
end.

Expand Down
Loading