Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
f391b31
feat(access): add access handling to chttpd
janl Jun 24, 2022
056f5fd
feat(access): add access to couch_db internal records
janl Jun 24, 2022
70ce692
feat(access): handle new records in couch_doc
janl Jun 24, 2022
ab6a083
feat(access): add new _users role for all authenticated users
janl Jun 24, 2022
7513142
feat(access): add access query server
janl Jun 24, 2022
3778644
feat(access): expand couch_btree / bt_engine to handle access
janl Jun 24, 2022
fb8a0c5
feat(access): handle access in couch_db[_updater]
janl Jun 24, 2022
b423bd9
feat(access): add util functions
janl Jun 25, 2022
d624115
feat(access): adjust existing tests
janl Jun 25, 2022
2d4b27d
feat(access): add mrview machinery
janl Jun 25, 2022
55f816b
feat(access): add access tests
janl Jun 25, 2022
af0b1f3
feat(access): add access handling to replicator
janl Jun 27, 2022
e6a2d98
feat(access): add access handling to ddoc cache
janl Jun 27, 2022
4e0fff9
feat(access): add access handling to fabric
janl Jun 27, 2022
47a4f56
feat(access): additional test fixes
janl Jun 27, 2022
77870be
fix: make tests pass again
janl Jul 23, 2022
fea0e48
feat(access): add global off switch
janl Aug 6, 2022
e3d1efc
doc(access): leave todo for missing implementation detail
janl Aug 6, 2022
0133b66
chore(access): remove old comment
janl Aug 6, 2022
729168f
fix(access): use minimal info from prev rev
janl Aug 6, 2022
bee1a7e
chore(access): style notes
janl Aug 6, 2022
1f3f5e7
doc(access): add todos
janl Aug 6, 2022
fca4350
fix(access): opt-out switch
janl Aug 6, 2022
e79b1ff
test(access): test disable access config
janl Aug 6, 2022
6117a0e
fix(access): elixir tests
janl Aug 6, 2022
67851e8
chore(access): erlfmt
janl Aug 6, 2022
9f2c417
chore: remove comments and stale todo entries
janl Aug 20, 2022
df14856
fix(access) elixir tests again
janl Aug 20, 2022
77a32a4
fix: simplify
janl Aug 20, 2022
0645933
chore: append _users role instead of prepending it
janl Nov 11, 2022
520eb8a
fix: restore previous function signature
janl Nov 11, 2022
280d8c8
fix: add function signature change to new open_docs_rev/3
janl Nov 12, 2022
418a41d
wip
janl May 22, 2023
cac8116
add perf regression test
janl Jun 18, 2023
fdd1b64
chore: clean up after renaming commit
janl Jun 18, 2023
a266fc0
fix: perf insert optimisation bypass
janl Jul 8, 2023
1ade764
chore: cleanup
janl Jul 8, 2023
06e7971
refactor: simplify detecting updated ddocs
janl Jul 8, 2023
f9ef7ee
fix: only process deleted docs in _access views
janl Jul 8, 2023
a9473ee
chore: revert debug code
janl Jul 8, 2023
273b52d
chore: remove debug log
janl Jul 8, 2023
4cf5f08
chore: undo whitespace
janl Jul 8, 2023
6a151de
refactor: resolve layer boundary violation
janl Jul 8, 2023
d680065
chore: remove debug comments
janl Jul 8, 2023
e8d75fa
feat: add _users role for jwt auth
janl Jul 8, 2023
517e742
chore: undo unwanted ws changes
janl Jul 8, 2023
8c3005c
chore: remove debugging comments
janl Jul 8, 2023
8f58b31
chore: remove debug comments
janl Jul 8, 2023
527acd6
chore: remove debug comments
janl Jul 8, 2023
f4d77b9
chore: remove debug code
janl Jul 8, 2023
38bd55a
Revert "chore: remove debug code"
janl Jul 8, 2023
c41a971
chore: remove debugging comment
janl Jul 8, 2023
ad301b9
refactor: simplify
janl Jul 8, 2023
b73313d
refactor: simplify
janl Jul 8, 2023
8c5f7ac
debugging on three sites
janl Jul 11, 2023
a76f044
fix outstanding test cases
janl Jul 12, 2023
eb55652
chore: lint
janl Jul 12, 2023
a06fb6b
force new CI run
janl Jul 13, 2023
1e5d9e7
re-enable fixed test
janl Jul 28, 2023
b32434a
fix remaining access tests
janl Jul 28, 2023
30ae3de
chore: fix compiler warnings
janl Aug 7, 2023
6ce958d
chore: address various rerview notes by @rnewson
janl Aug 17, 2023
60825cf
chore: erlfmt
janl Jan 10, 2026
33767e7
Revert "feat(access): add access handling to ddoc cache"
janl Jan 20, 2026
5cc6080
test: update tests to match new RFC semantics
janl Jan 27, 2026
629bcaa
chore: take out ddoc_cache special casing from access validation
janl Jan 27, 2026
9ddf130
feat: move access option into db props
janl Jan 28, 2026
33060d1
feat: block non-access ddocs from non-admins
janl Jan 29, 2026
305e2fa
fix: remove DoS vector
janl Jan 29, 2026
627cb63
feat: add access handling to show/list/update w/tests
janl Jan 29, 2026
add9d19
feat: add map body support to test_request and update access tests to…
janl Jan 30, 2026
33afe8b
feat: remove `access` field from #db header, using props: [] instead
janl Jan 30, 2026
b78dd39
wip: add access handling to _bulk_get (batched edition)
janl Jan 30, 2026
8125cbc
chore: make access tests more deterministic
janl Jan 30, 2026
324181e
feat: add access handling to _bulk_get (batched)
janl Jan 30, 2026
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 @@ -508,6 +508,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]
;enable = 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 @@ -1084,6 +1084,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
48 changes: 39 additions & 9 deletions src/chttpd/src/chttpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ create_db_req(#httpd{} = Req, DbName) ->
couch_httpd:verify_is_server_admin(Req),
ShardsOpt = parse_shards_opt(Req),
EngineOpt = parse_engine_opt(Req),
DbProps = parse_partitioned_opt(Req),
DbProps = parse_db_props(Req),
Options = lists:append([ShardsOpt, [{props, DbProps}], EngineOpt]),
DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
case fabric:create_db(DbName, Options) of
Expand Down Expand Up @@ -1021,16 +1021,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}]),
Rev = chttpd:qs_value(Req, "rev"),
case Rev 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 = Rev, 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 @@ -1479,6 +1481,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 @@ -2031,10 +2035,12 @@ parse_shards_opt("placement", Req, Default) ->
end;
parse_shards_opt(Param, Req, 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
true -> Val;
false -> throw({bad_request, Err})
true ->
Val;
false ->
Err = ?l2b(["The `", Param, "` value should be a positive integer."]),
throw({bad_request, Err})
end.

parse_engine_opt(Req) ->
Expand All @@ -2051,6 +2057,30 @@ parse_engine_opt(Req) ->
end
end.

parse_access_opt(Req) ->
AccessValue = chttpd:qs_value(Req, "access", "false"),
AccessEnabled = config:get_boolean("per_doc_access", "enable", false),
case AccessValue of
"true" ->
case AccessEnabled of
false ->
Err = <<"The `access` option is not available on this CouchDB installation.">>,
throw({bad_request, Err});
_ ->
[{access, true}]
end;
"false" ->
[];
_ ->
Err = <<"The `access` value should be a boolean.">>,
throw({bad_request, Err})
end.

parse_db_props(Req) ->
Partitioned = parse_partitioned_opt(Req),
Access = parse_access_opt(Req),
Partitioned ++ Access.

parse_partitioned_opt(Req) ->
case chttpd:qs_value(Req, "partitioned") of
undefined ->
Expand Down
3 changes: 3 additions & 0 deletions src/chttpd/src/chttpd_show.erl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ handle_doc_show(Req, Db, DDoc, ShowName, Doc) ->

handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId) ->
%% Will throw an exception if the _show handler is missing
ok = couch_db:validate_access(Db, DDoc),
couch_util:get_nested_json_value(DDoc#doc.body, [<<"shows">>, ShowName]),
% get responder for ddoc/showname
CurrentEtag = show_etag(Req, Doc, DDoc, []),
Expand Down Expand Up @@ -137,6 +138,7 @@ handle_doc_update_req(Req, _Db, _DDoc) ->

send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) ->
%% Will throw an exception if the _update handler is missing
% ok = couch_db:validate_access(Db, DDoc),
couch_util:get_nested_json_value(DDoc#doc.body, [<<"updates">>, UpdateName]),
JsonReq = chttpd_external:json_req_obj(Req, Db, DocId),
JsonDoc = couch_query_servers:json_doc(Doc),
Expand Down Expand Up @@ -249,6 +251,7 @@ handle_view_list_req(Req, _Db, _DDoc) ->

handle_view_list(Req, Db, DDoc, LName, {ViewDesignName, ViewName}, Keys) ->
%% Will throw an exception if the _list handler is missing
ok = couch_db:validate_access(Db, DDoc),
couch_util:get_nested_json_value(DDoc#doc.body, [<<"lists">>, LName]),
DbName = couch_db:name(Db),
{ok, VDoc} = ddoc_cache:open(DbName, <<"_design/", ViewDesignName/binary>>),
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 @@ -66,6 +66,7 @@ design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
fabric_query_view(Db, Req, DDoc, ViewName, Args).

fabric_query_view(Db, Req, DDoc, ViewName, Args) ->
ok = couch_db:validate_access(Db, DDoc),
Max = chttpd:chunked_response_buffer_size(),
VAcc = #vacc{db = Db, req = Req, threshold = Max},
Options = [{user_ctx, Req#httpd.user_ctx}],
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 = []
}).

-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 @@ -200,7 +203,8 @@
ptr,
seq,
sizes = #size_info{},
atts = []
atts = [],
access = []
}).

-record (fabric_changes_acc, {
Expand Down
137 changes: 137 additions & 0 deletions src/couch/src/couch_access_native_proc.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
% 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 ->
[[], []]
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 @@ -667,20 +667,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 @@ -717,21 +721,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 @@ -749,7 +759,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 @@ -758,6 +769,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
Loading