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 -> [];