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

Per document access control #4673

Open
wants to merge 62 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
11302dd
feat(access): add access handling to chttpd
janl Jun 24, 2022
3d225cc
feat(access): add access to couch_db internal records
janl Jun 24, 2022
d182a9c
feat(access): handle new records in couch_doc
janl Jun 24, 2022
973c407
feat(access): add new _users role for all authenticated users
janl Jun 24, 2022
3231b08
feat(access): add access query server
janl Jun 24, 2022
10525e9
feat(access): expand couch_btree / bt_engine to handle access
janl Jun 24, 2022
5cb4b09
feat(access): handle access in couch_db[_updater]
janl Jun 24, 2022
a76a1a9
feat(access): add util functions
janl Jun 25, 2022
033683b
feat(access): adjust existing tests
janl Jun 25, 2022
698f268
feat(access): add mrview machinery
janl Jun 25, 2022
8a0d901
feat(access): add access tests
janl Jun 25, 2022
eb2f0e4
feat(access): add access handling to replicator
janl Jun 27, 2022
26e6c78
feat(access): add access handling to ddoc cache
janl Jun 27, 2022
db803af
feat(access): add access handling to fabric
janl Jun 27, 2022
12abedf
feat(access): additional test fixes
janl Jun 27, 2022
0109b04
fix: make tests pass again
janl Jul 23, 2022
6cc46f4
feat(access): add global off switch
janl Aug 6, 2022
c4f51b7
doc(access): leave todo for missing implementation detail
janl Aug 6, 2022
66bbefa
chore(access): remove old comment
janl Aug 6, 2022
6fd27aa
fix(access): use minimal info from prev rev
janl Aug 6, 2022
c105296
chore(access): style notes
janl Aug 6, 2022
1bb8f00
doc(access): add todos
janl Aug 6, 2022
0a43ca9
fix(access): opt-out switch
janl Aug 6, 2022
afc4aaa
test(access): test disable access config
janl Aug 6, 2022
88e7a90
fix(access): elixir tests
janl Aug 6, 2022
99d1945
chore(access): erlfmt
janl Aug 6, 2022
70e3934
chore: remove comments and stale todo entries
janl Aug 20, 2022
bd643de
fix(access) elixir tests again
janl Aug 20, 2022
0005e36
fix: simplify
janl Aug 20, 2022
b9b057c
chore: append _users role instead of prepending it
janl Nov 11, 2022
fdfa229
fix: restore previous function signature
janl Nov 11, 2022
f7747d7
fix: add function signature change to new open_docs_rev/3
janl Nov 12, 2022
f258df4
wip
janl May 22, 2023
a97c7d7
add perf regression test
janl Jun 18, 2023
ad41b24
chore: clean up after renaming commit
janl Jun 18, 2023
82eb860
fix: perf insert optimisation bypass
janl Jul 8, 2023
0f6e2f7
chore: cleanup
janl Jul 8, 2023
66fdc2a
refactor: simplify detecting updated ddocs
janl Jul 8, 2023
16680e7
fix: only process deleted docs in _access views
janl Jul 8, 2023
e96415a
chore: revert debug code
janl Jul 8, 2023
7f46c70
chore: remove debug log
janl Jul 8, 2023
2b53bce
chore: undo whitespace
janl Jul 8, 2023
441dc28
refactor: resolve layer boundary violation
janl Jul 8, 2023
8ec3a0e
chore: remove debug comments
janl Jul 8, 2023
f8586a8
feat: add _users role for jwt auth
janl Jul 8, 2023
f6e409f
chore: undo unwanted ws changes
janl Jul 8, 2023
5bc6d1b
chore: remove debugging comments
janl Jul 8, 2023
88dd94a
chore: remove debug comments
janl Jul 8, 2023
3db3dde
chore: remove debug comments
janl Jul 8, 2023
c3cdc60
chore: remove debug code
janl Jul 8, 2023
fa31ba9
Revert "chore: remove debug code"
janl Jul 8, 2023
c76b511
chore: remove debugging comment
janl Jul 8, 2023
f3a0110
refactor: simplify
janl Jul 8, 2023
42638d6
refactor: simplify
janl Jul 8, 2023
6cec6b6
debugging on three sites
janl Jul 11, 2023
b6b61a0
fix outstanding test cases
janl Jul 12, 2023
92dc3d1
chore: lint
janl Jul 12, 2023
fae4a03
force new CI run
janl Jul 13, 2023
d0eecff
re-enable fixed test
janl Jul 28, 2023
6d1d1fa
fix remaining access tests
janl Jul 28, 2023
072d467
chore: fix compiler warnings
janl Aug 7, 2023
b1c6308
chore: address various rerview notes by @rnewson
janl Aug 17, 2023
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 @@ -402,6 +402,10 @@ authentication_db = _users
; max_iterations, password_scheme, password_regexp, proxy_use_secret,
; public_fields, secret, users_db_public, cookie_domain, same_site

; Per document access settings
[per_doc_access]
;enabled = false
janl marked this conversation as resolved.
Show resolved Hide resolved

; 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 @@ -1034,6 +1034,8 @@ error_info({bad_request, Error, Reason}) ->
{400, couch_util:to_binary(Error), couch_util:to_binary(Reason)};
error_info({query_parse_error, Reason}) ->
{400, <<"query_parse_error">>, Reason};
error_info(access) ->
Copy link
Member

Choose a reason for hiding this comment

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

this is too generic for me, per_doc_access_denied or something similarly descriptive please.

{403, <<"forbidden">>, <<"access">>};
error_info(database_does_not_exist) ->
{404, <<"not_found">>, <<"Database does not exist.">>};
error_info(not_found) ->
Expand Down
30 changes: 25 additions & 5 deletions src/chttpd/src/chttpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -958,16 +958,18 @@ view_cb(Msg, Acc) ->
couch_mrview_http:view_cb(Msg, Acc).

db_doc_req(#httpd{method = 'DELETE'} = Req, Db, DocId) ->
% check for the existence of the doc to handle the 404 case.
couch_doc_open(Db, DocId, nil, []),
case chttpd:qs_value(Req, "rev") of
% fetch the old doc revision, so we can compare access control
% in send_update_doc() later.
Doc0 = couch_doc_open(Db, DocId, nil, [{user_ctx, Req#httpd.user_ctx}]),
Revs = chttpd:qs_value(Req, "rev"),
janl marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1417,6 +1419,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 @@ -1939,6 +1943,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 @@ -1967,6 +1972,21 @@ parse_shards_opt("placement", Req, Default) ->
throw({bad_request, Err})
end
end;
parse_shards_opt("access", Req, Value) when is_list(Value) ->
janl marked this conversation as resolved.
Show resolved Hide resolved
parse_shards_opt("access", Req, list_to_existing_atom(Value));
parse_shards_opt("access", _Req, Value) when Value =:= true ->
janl marked this conversation as resolved.
Show resolved Hide resolved
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."]),
janl marked this conversation as resolved.
Show resolved Hide resolved
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) ->
Val = chttpd:qs_value(Req, Param, Default),
Err = ?l2b(["The `", Param, "` value should be a positive integer."]),
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 @@ -67,7 +67,8 @@
-record(doc_info, {
id = <<"">>,
high_seq = 0,
revs = [] % rev_info
revs = [], % rev_info
access = []
Copy link
Member

Choose a reason for hiding this comment

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

is doc_info this passed between nodes?

}).

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

-record(httpd, {
Expand Down Expand Up @@ -124,7 +126,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 @@ -210,7 +213,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}).
Copy link
Member

Choose a reason for hiding this comment

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

is a 5 second timeout appropriate here?

Copy link
Member Author

Choose a reason for hiding this comment

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

it probably needs to be configurable like other query server timeouts


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
janl marked this conversation as resolved.
Show resolved Hide resolved
% _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
7 changes: 7 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,12 @@ full_reduce(#btree{root = nil, reduce = Reduce}) ->
full_reduce(#btree{root = Root}) ->
{ok, element(2, Root)}.

full_reduce_with_options(Bt, Options) ->
CountFun = fun(_SeqStart, PartialReds, 0) ->
{ok, couch_btree:final_reduce(Bt, PartialReds)}
end,
fold_reduce(Bt, CountFun, 0, Options).

size(#btree{root = nil}) ->
0;
size(#btree{root = {_P, _Red}}) ->
Expand Down
Loading