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 2019/11/23 12:58:24 UTC
[couchdb] 03/04: feat: ignore access ddocs for views/shows/lists,
etc.
This is an automated email from the ASF dual-hosted git repository.
jan pushed a commit to branch access
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 2000daae2d8b84b7a7e3d3c1c8606c12bd97188c
Author: Jan Lehnardt <ja...@apache.org>
AuthorDate: Sun Oct 20 11:18:18 2019 +0200
feat: ignore access ddocs for views/shows/lists, etc.
---
src/chttpd/src/chttpd_show.erl | 10 ++++++
src/chttpd/src/chttpd_view.erl | 12 ++-----
src/couch/src/couch_changes.erl | 2 ++
src/couch/src/couch_db.erl | 24 +++++++++----
src/couch/src/couch_db_updater.erl | 11 ++++--
src/couch/src/couch_doc.erl | 11 +++++-
src/couch/src/couch_util.erl | 7 ++++
src/couch/test/couchdb_access_tests.erl | 39 ++++++++++++++++++++--
.../src/ddoc_cache_entry_validation_funs.erl | 4 ++-
9 files changed, 98 insertions(+), 22 deletions(-)
diff --git a/src/chttpd/src/chttpd_show.erl b/src/chttpd/src/chttpd_show.erl
index c6d232c..bb1d52f 100644
--- a/src/chttpd/src/chttpd_show.erl
+++ b/src/chttpd/src/chttpd_show.erl
@@ -34,6 +34,8 @@ handle_doc_show_req(#httpd{
path_parts=[_, _, _, _, ShowName, DocId]
}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
+
% open the doc
Options = [conflicts, {user_ctx, Req#httpd.user_ctx}],
Doc = maybe_open_doc(Db, DocId, Options),
@@ -46,6 +48,8 @@ handle_doc_show_req(#httpd{
path_parts=[_, _, _, _, ShowName, DocId|Rest]
}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
+
DocParts = [DocId|Rest],
DocId1 = ?l2b(string:join([?b2l(P)|| P <- DocParts], "/")),
@@ -103,11 +107,13 @@ show_etag(#httpd{user_ctx=UserCtx}=Req, Doc, DDoc, More) ->
handle_doc_update_req(#httpd{
path_parts=[_, _, _, _, UpdateName]
}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null);
handle_doc_update_req(#httpd{
path_parts=[_, _, _, _, UpdateName | DocIdParts]
}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
DocId = ?l2b(string:join([?b2l(P) || P <- DocIdParts], "/")),
Options = [conflicts, {user_ctx, Req#httpd.user_ctx}],
Doc = maybe_open_doc(Db, DocId, Options),
@@ -159,12 +165,14 @@ handle_view_list_req(#httpd{method=Method,
path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc)
when Method =:= 'GET' orelse Method =:= 'OPTIONS' ->
Keys = chttpd:qs_json_value(Req, "keys", undefined),
+ ok = couch_util:validate_design_access(DDoc),
handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, Keys);
% view-list request with view and list from different design docs.
handle_view_list_req(#httpd{method=Method,
path_parts=[_, _, _, _, ListName, DesignName, ViewName]}=Req, Db, DDoc)
when Method =:= 'GET' orelse Method =:= 'OPTIONS' ->
+ ok = couch_util:validate_design_access(DDoc),
Keys = chttpd:qs_json_value(Req, "keys", undefined),
handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, Keys);
@@ -174,6 +182,7 @@ handle_view_list_req(#httpd{method=Method}=Req, _Db, _DDoc)
handle_view_list_req(#httpd{method='POST',
path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
chttpd:validate_ctype(Req, "application/json"),
ReqBody = chttpd:body(Req),
{Props2} = ?JSON_DECODE(ReqBody),
@@ -183,6 +192,7 @@ handle_view_list_req(#httpd{method='POST',
handle_view_list_req(#httpd{method='POST',
path_parts=[_, _, _, _, ListName, DesignName, ViewName]}=Req, Db, DDoc) ->
+ ok = couch_util:validate_design_access(DDoc),
chttpd:validate_ctype(Req, "application/json"),
ReqBody = chttpd:body(Req),
{Props2} = ?JSON_DECODE(ReqBody),
diff --git a/src/chttpd/src/chttpd_view.erl b/src/chttpd/src/chttpd_view.erl
index 4d1b80a..6ee21ba 100644
--- a/src/chttpd/src/chttpd_view.erl
+++ b/src/chttpd/src/chttpd_view.erl
@@ -16,12 +16,6 @@
-export([handle_view_req/3, handle_temp_view_req/2]).
-validate_design_access(DDoc) ->
- is_users_ddoc(DDoc).
-
-is_users_ddoc(#doc{access=[<<"_users">>]}) -> ok;
-is_users_ddoc(_) -> throw({forbidden, <<"per-user ddoc access">>}).
-
multi_query_view(Req, Db, DDoc, ViewName, Queries) ->
Args0 = couch_mrview_http:parse_params(Req, undefined),
{ok, #mrst{views=Views}} = couch_mrview_util:ddoc_to_mrst(Db, DDoc),
@@ -57,7 +51,7 @@ design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
handle_view_req(#httpd{method='POST',
path_parts=[_, _, _, _, ViewName, <<"queries">>]}=Req, Db, DDoc) ->
- ok = validate_design_access(DDoc),
+ ok = couch_util:validate_design_access(DDoc),
chttpd:validate_ctype(Req, "application/json"),
Props = couch_httpd:json_body_obj(Req),
case couch_mrview_util:get_view_queries(Props) of
@@ -74,14 +68,14 @@ handle_view_req(#httpd{path_parts=[_, _, _, _, _, <<"queries">>]}=Req,
handle_view_req(#httpd{method='GET',
path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) ->
- ok = validate_design_access(DDoc),
+ ok = couch_util:validate_design_access(DDoc),
couch_stats:increment_counter([couchdb, httpd, view_reads]),
Keys = chttpd:qs_json_value(Req, "keys", undefined),
design_doc_view(Req, Db, DDoc, ViewName, Keys);
handle_view_req(#httpd{method='POST',
path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) ->
- ok = validate_design_access(DDoc),
+ ok = couch_util:validate_design_access(DDoc),
chttpd:validate_ctype(Req, "application/json"),
Props = couch_httpd:json_body_obj(Req),
Keys = couch_mrview_util:get_view_keys(Props),
diff --git a/src/couch/src/couch_changes.erl b/src/couch/src/couch_changes.erl
index c5b5edf..da225bb 100644
--- a/src/couch/src/couch_changes.erl
+++ b/src/couch/src/couch_changes.erl
@@ -213,6 +213,7 @@ configure_filter("_view", Style, Req, Db) ->
case [?l2b(couch_httpd:unquote(Part)) || Part <- ViewNameParts] of
[DName, VName] ->
{ok, DDoc} = open_ddoc(Db, <<"_design/", DName/binary>>),
+ ok = couch_util:validate_design_access(DDoc),
check_member_exists(DDoc, [<<"views">>, VName]),
FilterType = try
true = couch_util:get_nested_json_value(
@@ -245,6 +246,7 @@ configure_filter(FilterName, Style, Req, Db) ->
case [?l2b(couch_httpd:unquote(Part)) || Part <- FilterNameParts] of
[DName, FName] ->
{ok, DDoc} = open_ddoc(Db, <<"_design/", DName/binary>>),
+ ok = couch_util:validate_design_access(DDoc),
check_member_exists(DDoc, [<<"filters">>, FName]),
case couch_db:is_clustered(Db) of
true ->
diff --git a/src/couch/src/couch_db.erl b/src/couch/src/couch_db.erl
index a4d0e67..47a35f1 100644
--- a/src/couch/src/couch_db.erl
+++ b/src/couch/src/couch_db.erl
@@ -648,7 +648,10 @@ get_design_docs(#db{name = <<"shards/", _/binary>> = ShardDbName}) ->
Response
end;
get_design_docs(#db{} = Db) ->
- FoldFun = fun(FDI, Acc) -> {ok, [FDI | Acc]} end,
+ FoldFun = fun(FDI, Acc) ->
+ couch_log:info("~n===========~nget_design_docs() FDI: ~p~n--------------~n", [FDI]),
+ {ok, [FDI | Acc]}
+ end,
{ok, Docs} = fold_design_docs(Db, FoldFun, [], []),
{ok, lists:reverse(Docs)}.
@@ -902,12 +905,18 @@ group_alike_docs([Doc|Rest], [Bucket|RestBuckets]) ->
validate_doc_update(#db{}=Db, #doc{id= <<"_design/",_/binary>>}=Doc, _GetDiskDocFun) ->
- case catch check_is_admin(Db) of
- ok -> validate_ddoc(Db#db.name, Doc);
- Error -> Error
- end;
-validate_doc_update(#db{validate_doc_funs = undefined} = Db, Doc, Fun) ->
+ case couch_doc:has_access(Doc) of
+ true ->
+ validate_ddoc(Db#db.name, Doc);
+ _Else ->
+ case catch check_is_admin(Db) of
+ ok -> validate_ddoc(Db#db.name, Doc);
+ Error -> Error
+ end
+ end;
+validate_doc_update(#db{validate_doc_funs = undefined, name = Name} = Db, Doc, Fun) ->
ValidationFuns = load_validation_funs(Db),
+ couch_log:info("~n====== attemptint to write Doc: ~p, VDUs: ~p, for Db: ~p~n===========~n", [Doc, ValidationFuns, Name]),
validate_doc_update(Db#db{validate_doc_funs=ValidationFuns}, Doc, Fun);
validate_doc_update(#db{validate_doc_funs=[]}=Db, _Doc, _GetDiskDocFun) ->
ok;
@@ -955,11 +964,13 @@ validate_doc_update_int(Db, Doc, GetDiskDocFun) ->
% to be safe, spawn a middleman here
load_validation_funs(#db{main_pid=Pid, name = <<"shards/", _/binary>>}=Db) ->
+ couch_log:info("~n~nload_validation_funs_shards~n~n", []),
{_, Ref} = spawn_monitor(fun() ->
exit(ddoc_cache:open(mem3:dbname(Db#db.name), validation_funs))
end),
receive
{'DOWN', Ref, _, _, {ok, Funs}} ->
+ couch_log:info("~n~nloaded_validation_funs_shards: ~p~n~n", [Funs]),
gen_server:cast(Pid, {load_validation_funs, Funs}),
Funs;
{'DOWN', Ref, _, _, {database_does_not_exist, _StackTrace}} ->
@@ -971,6 +982,7 @@ load_validation_funs(#db{main_pid=Pid, name = <<"shards/", _/binary>>}=Db) ->
end;
load_validation_funs(#db{main_pid=Pid}=Db) ->
{ok, DDocInfos} = get_design_docs(Db),
+ couch_log:info("~n~nload_validation_funs() -> DDocInfos: ~p~n~n", [DDocInfos]),
OpenDocs = fun
(#full_doc_info{}=D) ->
{ok, Doc} = open_doc_int(Db, D, [ejson_body]),
diff --git a/src/couch/src/couch_db_updater.erl b/src/couch/src/couch_db_updater.erl
index 245554b..75e374f 100644
--- a/src/couch/src/couch_db_updater.erl
+++ b/src/couch/src/couch_db_updater.erl
@@ -198,9 +198,12 @@ handle_info({update_docs, Client, GroupedDocs, NonRepDocs, MergeConflicts,
couch_event:notify(Db2#db.name, {ddoc_updated, DDocId})
end, UpdatedDDocIds),
couch_event:notify(Db2#db.name, ddoc_updated),
+ couch_log:info("~n--------~nupdating ddoc cache with ids: ~p", [UpdatedDDocIds]),
ddoc_cache:refresh(Db2#db.name, UpdatedDDocIds),
refresh_validate_doc_funs(Db2);
false ->
+ couch_log:info("~n--------~nupdating ddoc cache NOPE", []),
+
Db2
end,
{noreply, Db3, hibernate_if_no_idle_limit()}
@@ -339,6 +342,7 @@ refresh_validate_doc_funs(Db0) ->
Db = Db0#db{user_ctx=?ADMIN_USER},
{ok, DesignDocs} = couch_db:get_design_docs(Db),
% TODO: filter out non-admin ddocs
+ couch_log:info("~n~nrefresh_validate_doc_funs() -> DesignDocs: ~p~n~n", [DesignDocs]),
ProcessDocFuns = lists:flatmap(
fun(DesignDocInfo) ->
{ok, DesignDoc} = couch_db:open_doc_int(
@@ -622,12 +626,13 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts, FullCommit) ->
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),
+ end, NonAccessIds),
{ok, commit_data(Db1, not FullCommit), UpdatedDDocIds}.
diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl
index 1ec31e2..6e4b38c 100644
--- a/src/couch/src/couch_doc.erl
+++ b/src/couch/src/couch_doc.erl
@@ -25,7 +25,7 @@
-export([with_ejson_body/1]).
-export([is_deleted/1]).
-
+-export([has_access/1, has_no_access/1]).
-include_lib("couch/include/couch_db.hrl").
@@ -404,6 +404,15 @@ get_access({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) ->
diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl
index 62e17ce..4fa2fde 100644
--- a/src/couch/src/couch_util.erl
+++ b/src/couch/src/couch_util.erl
@@ -39,6 +39,7 @@
-export([check_config_blacklist/1]).
-export([check_md5/2]).
-export([set_mqd_off_heap/1]).
+-export([validate_design_access/1]).
-include_lib("couch/include/couch_db.hrl").
@@ -749,3 +750,9 @@ check_config_blacklist(Section) ->
_ ->
ok
end.
+
+validate_design_access(DDoc) ->
+ is_users_ddoc(DDoc).
+
+is_users_ddoc(#doc{access=[<<"_users">>]}) -> ok;
+is_users_ddoc(_) -> throw({forbidden, <<"per-user ddoc access">>}).
diff --git a/src/couch/test/couchdb_access_tests.erl b/src/couch/test/couchdb_access_tests.erl
index a3c692b..41b300c 100644
--- a/src/couch/test/couchdb_access_tests.erl
+++ b/src/couch/test/couchdb_access_tests.erl
@@ -40,6 +40,7 @@ before_all() ->
Couch = test_util:start_couch([chttpd]),
Hashed = couch_passwords:hash_admin_password("a"),
ok = config:set("admins", "a", binary_to_list(Hashed), _Persist=false),
+ % ok = config:set("log", "level", "debug", _Persist=false),
% cleanup and setup
{ok, _, _, _} = test_request:delete(url() ++ "/db", ?ADMIN_REQ_HEADERS),
@@ -70,6 +71,8 @@ access_test_() ->
fun should_let_admin_create_doc_without_access/2,
fun should_let_user_create_doc_for_themselves/2,
fun should_not_let_user_create_doc_for_someone_else/2,
+ fun should_let_user_create_access_ddoc/2,
+ fun access_ddoc_should_have_no_effects/2,
% Doc updates
fun users_with_access_can_update_doc/2,
@@ -93,7 +96,7 @@ access_test_() ->
fun should_let_admin_fetch_all_docs/2,
fun should_let_user_fetch_their_own_all_docs/2,
% potential future feature
- %fun should_let_user_fetch_their_own_all_docs_plus_users_ddocs/2%,
+ % % % fun should_let_user_fetch_their_own_all_docs_plus_users_ddocs/2%,
% _changes
fun should_let_admin_fetch_changes/2,
@@ -104,7 +107,7 @@ access_test_() ->
fun should_not_allow_user_access_ddoc_view_request/2,
fun should_allow_admin_users_access_ddoc_view_request/2,
fun should_allow_user_users_access_ddoc_view_request/2
-
+
% TODO: create test db with role and not _users in _security.members
% and make sure a user in that group can access while a user not
% in that group cant
@@ -152,6 +155,38 @@ should_not_let_user_create_doc_for_someone_else(_PortType, Url) ->
?USERY_REQ_HEADERS, "{\"a\":1,\"_access\":[\"x\"]}"),
?_assertEqual(403, Code).
+should_let_user_create_access_ddoc(_PortType, Url) ->
+ {ok, Code, _, _} = test_request:put(Url ++ "/db/_design/dx",
+ ?USERX_REQ_HEADERS, "{\"a\":1,\"_access\":[\"x\"]}"),
+ ?_assertEqual(201, Code).
+
+access_ddoc_should_have_no_effects(_PortType, Url) ->
+ Ddoc = "{ \"_access\":[\"x\"], \"validate_doc_update\": \"function(newDoc, oldDoc, userCtx) { throw({unauthorized: 'throw error'})}\", \"views\": { \"foo\": { \"map\": \"function(doc) { emit(doc._id) }\" } }, \"shows\": { \"boo\": \"function() {}\" }, \"lists\": { \"hoo\": \"function() {}\" }, \"update\": { \"goo\": \"function() {}\" }, \"filters\": { \"loo\": \"function() {}\" } }",
+ {ok, Code, _, _} = test_request:put(Url ++ "/db/_design/dx",
+ ?USERX_REQ_HEADERS, Ddoc),
+ ?_assertEqual(201, Code),
+ {ok, Code1, _, _} = test_request:put(Url ++ "/db/b",
+ ?USERX_REQ_HEADERS, "{\"a\":1,\"_access\":[\"x\"]}"),
+ ?_assertEqual(401, Code1),
+ {ok, Code2, _, _} = test_request:get(Url ++ "/db/_design/dx/_view/foo",
+ ?USERX_REQ_HEADERS),
+ ?_assertEqual(403, Code2),
+ {ok, Code3, _, _} = test_request:get(Url ++ "/db/_design/dx/_show/boo/b",
+ ?USERX_REQ_HEADERS),
+ ?_assertEqual(403, Code3),
+ {ok, Code4, _, _} = test_request:get(Url ++ "/db/_design/dx/_list/hoo/foo",
+ ?USERX_REQ_HEADERS),
+ ?_assertEqual(403, Code4),
+ {ok, Code5, _, _} = test_request:post(Url ++ "/db/_design/dx/_update/goo",
+ ?USERX_REQ_HEADERS, ""),
+ ?_assertEqual(403, Code5),
+ {ok, Code6, _, _} = test_request:get(Url ++ "/db/_changes?filter=dx/loo",
+ ?USERX_REQ_HEADERS),
+ ?_assertEqual(403, Code6),
+ {ok, Code7, _, _} = test_request:get(Url ++ "/db/_changes?filter=_view&view=dx/foo",
+ ?USERX_REQ_HEADERS),
+ ?_assertEqual(403, Code7).
+
% Doc updates
users_with_access_can_update_doc(_PortType, Url) ->
diff --git a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
index 2182dea..c037a30 100644
--- a/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
+++ b/src/ddoc_cache/src/ddoc_cache_entry_validation_funs.erl
@@ -30,7 +30,9 @@ ddocid(_) ->
recover(DbName) ->
- {ok, DDocs} = fabric:design_docs(mem3:dbname(DbName)),
+ {ok, DDocs0} = fabric:design_docs(mem3:dbname(DbName)),
+ DDocs = lists:filter(fun couch_doc:has_no_access/1, DDocs0),
+ couch_log:info("~n~n~n~nrecover validation funs: ~p From DDocs0: ~p~n~n~n~n", [DDocs, DDocs0]),
Funs = lists:flatmap(fun(DDoc) ->
case couch_doc:get_validate_doc_fun(DDoc) of
nil -> [];