You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wo...@apache.org on 2017/10/03 21:22:39 UTC

[couchdb] branch 858-whitelist-shard-map-docids updated (0f2d0c1 -> 25c2b6e)

This is an automated email from the ASF dual-hosted git repository.

wohali pushed a change to branch 858-whitelist-shard-map-docids
in repository https://gitbox.apache.org/repos/asf/couchdb.git.


    omit 0f2d0c1  Configurably whitelist certain db/docids as valid
     new 25c2b6e  Whitelist system DB names as valid _dbs docids

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (0f2d0c1)
            \
             N -- N -- N   refs/heads/858-whitelist-shard-map-docids (25c2b6e)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/couch/test/couch_doc_json_tests.erl | 71 +++++++++++++++++++++++++++++++++
 src/couch/test/couch_doc_tests.erl      |  9 ++++-
 2 files changed, 79 insertions(+), 1 deletion(-)

-- 
To stop receiving notification emails like this one, please contact
['"commits@couchdb.apache.org" <co...@couchdb.apache.org>'].

[couchdb] 01/01: Whitelist system DB names as valid _dbs docids

Posted by wo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

wohali pushed a commit to branch 858-whitelist-shard-map-docids
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 25c2b6e5815094ec897da8020bacaa690af7718a
Author: Joan Touzet <jo...@atypical.net>
AuthorDate: Mon Oct 2 20:50:30 2017 -0400

    Whitelist system DB names as valid _dbs docids
    
    Currently, it is impossible to PUT/POST modified shard maps to any
    `_dbs/_*` document because the document _ids are reserved. This change
    permits these specific db/docid combinations as valid, so PUT/POST
    operations can succeed. The specific list comes from SYSTEM_DATABASES.
    
    Unit tests have been added.
---
 src/chttpd/src/chttpd_db.erl            |  9 ++--
 src/couch/src/couch_doc.erl             | 96 +++++++++++++++++++--------------
 src/couch/src/couch_httpd_db.erl        | 36 ++++++++-----
 src/couch/test/couch_doc_json_tests.erl | 71 ++++++++++++++++++++++++
 src/couch/test/couch_doc_tests.erl      | 16 ++++--
 5 files changed, 166 insertions(+), 62 deletions(-)

diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index c8826d5..fc03ec3 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -711,7 +711,8 @@ db_doc_req(#httpd{method='GET', mochi_req=MochiReq}=Req, Db, DocId) ->
 
 db_doc_req(#httpd{method='POST', user_ctx=Ctx}=Req, Db, DocId) ->
     couch_httpd:validate_referer(Req),
-    couch_doc:validate_docid(DocId),
+    DbName = couch_db:name(Db),
+    couch_doc:validate_docid(DocId, DbName),
     chttpd:validate_ctype(Req, "multipart/form-data"),
 
     W = chttpd:qs_value(Req, "w", integer_to_list(mem3:quorum(Db))),
@@ -766,9 +767,9 @@ db_doc_req(#httpd{method='PUT', user_ctx=Ctx}=Req, Db, DocId) ->
     #doc_query_args{
         update_type = UpdateType
     } = parse_doc_query(Req),
-    couch_doc:validate_docid(DocId),
-
     DbName = couch_db:name(Db),
+    couch_doc:validate_docid(DocId, DbName),
+
     W = chttpd:qs_value(Req, "w", integer_to_list(mem3:quorum(Db))),
     Options = [{user_ctx,Ctx}, {w,W}],
 
@@ -1243,7 +1244,7 @@ db_attachment_req(#httpd{method=Method, user_ctx=Ctx}=Req, Db, DocId, FileNamePa
                 % check for the existence of the doc to handle the 404 case.
                 couch_doc_open(Db, DocId, nil, [])
             end,
-            couch_doc:validate_docid(DocId),
+            couch_doc:validate_docid(DocId, couch_db:name(Db)),
             #doc{id=DocId};
         Rev ->
             case fabric:open_revs(Db, DocId, [Rev], [{user_ctx,Ctx}]) of
diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl
index eb96d44..6355865 100644
--- a/src/couch/src/couch_doc.erl
+++ b/src/couch/src/couch_doc.erl
@@ -13,8 +13,10 @@
 -module(couch_doc).
 
 -export([to_doc_info/1,to_doc_info_path/1,parse_rev/1,parse_revs/1,rev_to_str/1,revs_to_strs/1]).
--export([from_json_obj/1, from_json_obj_validate/1, to_json_obj/2,has_stubs/1, merge_stubs/2]).
--export([validate_docid/1, get_validate_doc_fun/1]).
+-export([from_json_obj/1, from_json_obj_validate/1]).
+-export([from_json_obj/2, from_json_obj_validate/2]).
+-export([to_json_obj/2, has_stubs/1, merge_stubs/2]).
+-export([validate_docid/1, validate_docid/2, get_validate_doc_fun/1]).
 -export([doc_from_multi_part_stream/2, doc_from_multi_part_stream/3]).
 -export([doc_from_multi_part_stream/4]).
 -export([doc_to_multi_part_stream/5, len_doc_to_multi_part_stream/4]).
@@ -126,8 +128,11 @@ doc_to_json_obj(#doc{id=Id,deleted=Del,body=Body,revs={Start, RevIds},
     }.
 
 from_json_obj_validate(EJson) ->
+    from_json_obj_validate(EJson, <<"">>).
+
+from_json_obj_validate(EJson, DbName) ->
     MaxSize = config:get_integer("couchdb", "max_document_size", 4294967296),
-    Doc = from_json_obj(EJson),
+    Doc = from_json_obj(EJson, DbName),
     case couch_ejson_size:encoded_size(Doc#doc.body) =< MaxSize of
         true ->
              validate_attachment_sizes(Doc#doc.atts),
@@ -149,9 +154,11 @@ validate_attachment_sizes(Atts) ->
 
 
 from_json_obj({Props}) ->
-    transfer_fields(Props, #doc{body=[]});
+    from_json_obj({Props}, <<"">>).
 
-from_json_obj(_Other) ->
+from_json_obj({Props}, DbName) ->
+    transfer_fields(Props, #doc{body=[]}, DbName);
+from_json_obj(_Other, _) ->
     throw({bad_request, "Document must be a JSON object"}).
 
 parse_revid(RevId) when size(RevId) =:= 32 ->
@@ -191,6 +198,15 @@ parse_revs(_) ->
     throw({bad_request, "Invalid list of revisions"}).
 
 
+validate_docid(DocId, DbName) ->
+    case DbName =:= ?l2b(config:get("mem3", "shards_db", "_dbs")) andalso
+        lists:member(DocId, ?SYSTEM_DATABASES) of
+        true ->
+            ok;
+        false ->
+            validate_docid(DocId)
+    end.
+
 validate_docid(<<"">>) ->
     throw({illegal_docid, <<"Document id must not be empty">>});
 validate_docid(<<"_design/">>) ->
@@ -228,28 +244,28 @@ validate_docid(Id) ->
     couch_log:debug("Document id is not a string: ~p", [Id]),
     throw({illegal_docid, <<"Document id must be a string">>}).
 
-transfer_fields([], #doc{body=Fields}=Doc) ->
+transfer_fields([], #doc{body=Fields}=Doc, _) ->
     % convert fields back to json object
     Doc#doc{body={lists:reverse(Fields)}};
 
-transfer_fields([{<<"_id">>, Id} | Rest], Doc) ->
-    validate_docid(Id),
-    transfer_fields(Rest, Doc#doc{id=Id});
+transfer_fields([{<<"_id">>, Id} | Rest], Doc, DbName) ->
+    validate_docid(Id, DbName),
+    transfer_fields(Rest, Doc#doc{id=Id}, DbName);
 
-transfer_fields([{<<"_rev">>, Rev} | Rest], #doc{revs={0, []}}=Doc) ->
+transfer_fields([{<<"_rev">>, Rev} | Rest], #doc{revs={0, []}}=Doc, DbName) ->
     {Pos, RevId} = parse_rev(Rev),
     transfer_fields(Rest,
-            Doc#doc{revs={Pos, [RevId]}});
+            Doc#doc{revs={Pos, [RevId]}}, DbName);
 
-transfer_fields([{<<"_rev">>, _Rev} | Rest], Doc) ->
+transfer_fields([{<<"_rev">>, _Rev} | Rest], Doc, DbName) ->
     % we already got the rev from the _revisions
-    transfer_fields(Rest,Doc);
+    transfer_fields(Rest, Doc, DbName);
 
-transfer_fields([{<<"_attachments">>, {JsonBins}} | Rest], Doc) ->
+transfer_fields([{<<"_attachments">>, {JsonBins}} | Rest], Doc, DbName) ->
     Atts = [couch_att:from_json(Name, Props) || {Name, {Props}} <- JsonBins],
-    transfer_fields(Rest, Doc#doc{atts=Atts});
+    transfer_fields(Rest, Doc#doc{atts=Atts}, DbName);
 
-transfer_fields([{<<"_revisions">>, {Props}} | Rest], Doc) ->
+transfer_fields([{<<"_revisions">>, {Props}} | Rest], Doc, DbName) ->
     RevIds = couch_util:get_value(<<"ids">>, Props),
     Start = couch_util:get_value(<<"start">>, Props),
     if not is_integer(Start) ->
@@ -262,45 +278,45 @@ transfer_fields([{<<"_revisions">>, {Props}} | Rest], Doc) ->
     [throw({doc_validation, "RevId isn't a string"}) ||
             RevId <- RevIds, not is_binary(RevId)],
     RevIds2 = [parse_revid(RevId) || RevId <- RevIds],
-    transfer_fields(Rest, Doc#doc{revs={Start, RevIds2}});
+    transfer_fields(Rest, Doc#doc{revs={Start, RevIds2}}, DbName);
 
-transfer_fields([{<<"_deleted">>, B} | Rest], Doc) when is_boolean(B) ->
-    transfer_fields(Rest, Doc#doc{deleted=B});
+transfer_fields([{<<"_deleted">>, B} | Rest], Doc, DbName) when is_boolean(B) ->
+    transfer_fields(Rest, Doc#doc{deleted=B}, DbName);
 
 % ignored fields
-transfer_fields([{<<"_revs_info">>, _} | Rest], Doc) ->
-    transfer_fields(Rest, Doc);
-transfer_fields([{<<"_local_seq">>, _} | Rest], Doc) ->
-    transfer_fields(Rest, Doc);
-transfer_fields([{<<"_conflicts">>, _} | Rest], Doc) ->
-    transfer_fields(Rest, Doc);
-transfer_fields([{<<"_deleted_conflicts">>, _} | Rest], Doc) ->
-    transfer_fields(Rest, Doc);
+transfer_fields([{<<"_revs_info">>, _} | Rest], Doc, DbName) ->
+    transfer_fields(Rest, Doc, DbName);
+transfer_fields([{<<"_local_seq">>, _} | Rest], Doc, DbName) ->
+    transfer_fields(Rest, Doc, DbName);
+transfer_fields([{<<"_conflicts">>, _} | Rest], Doc, DbName) ->
+    transfer_fields(Rest, Doc, DbName);
+transfer_fields([{<<"_deleted_conflicts">>, _} | Rest], Doc, DbName) ->
+    transfer_fields(Rest, Doc, DbName);
 
 % special fields for replication documents
 transfer_fields([{<<"_replication_state">>, _} = Field | Rest],
-    #doc{body=Fields} = Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]});
+    #doc{body=Fields} = Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName);
 transfer_fields([{<<"_replication_state_time">>, _} = Field | Rest],
-    #doc{body=Fields} = Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]});
+    #doc{body=Fields} = Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName);
 transfer_fields([{<<"_replication_state_reason">>, _} = Field | Rest],
-    #doc{body=Fields} = Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]});
+    #doc{body=Fields} = Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName);
 transfer_fields([{<<"_replication_id">>, _} = Field | Rest],
-    #doc{body=Fields} = Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]});
+    #doc{body=Fields} = Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName);
 transfer_fields([{<<"_replication_stats">>, _} = Field | Rest],
-    #doc{body=Fields} = Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]});
+    #doc{body=Fields} = Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName);
 
 % unknown special field
-transfer_fields([{<<"_",Name/binary>>, _} | _], _) ->
+transfer_fields([{<<"_",Name/binary>>, _} | _], _, _) ->
     throw({doc_validation,
             ?l2b(io_lib:format("Bad special document member: _~s", [Name]))});
 
-transfer_fields([Field | Rest], #doc{body=Fields}=Doc) ->
-    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}).
+transfer_fields([Field | Rest], #doc{body=Fields}=Doc, DbName) ->
+    transfer_fields(Rest, Doc#doc{body=[Field|Fields]}, DbName).
 
 to_doc_info(FullDocInfo) ->
     {DocInfo, _Path} = to_doc_info_path(FullDocInfo),
diff --git a/src/couch/src/couch_httpd_db.erl b/src/couch/src/couch_httpd_db.erl
index 34a1539..05e63ba 100644
--- a/src/couch/src/couch_httpd_db.erl
+++ b/src/couch/src/couch_httpd_db.erl
@@ -257,7 +257,8 @@ db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
 
 db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
     couch_httpd:validate_ctype(Req, "application/json"),
-    Doc = couch_doc:from_json_obj_validate(couch_httpd:json_body(Req)),
+    DbName = couch_db:name(Db),
+    Doc = couch_doc:from_json_obj_validate(couch_httpd:json_body(Req), DbName),
     validate_attachment_names(Doc),
     Doc2 = case Doc#doc.id of
         <<"">> ->
@@ -303,6 +304,7 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
     couch_stats:increment_counter([couchdb, httpd, bulk_requests]),
     couch_httpd:validate_ctype(Req, "application/json"),
     {JsonProps} = couch_httpd:json_body_obj(Req),
+    DbName = couch_db:name(Db),
     case couch_util:get_value(<<"docs">>, JsonProps) of
     undefined ->
         send_error(Req, 400, <<"bad_request">>, <<"Missing JSON list of 'docs'">>);
@@ -320,7 +322,7 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
         true ->
             Docs = lists:map(
                 fun({ObjProps} = JsonObj) ->
-                    Doc = couch_doc:from_json_obj_validate(JsonObj),
+                    Doc = couch_doc:from_json_obj_validate(JsonObj, DbName),
                     validate_attachment_names(Doc),
                     Id = case Doc#doc.id of
                         <<>> -> couch_uuids:new();
@@ -354,7 +356,7 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
             end;
         false ->
             Docs = lists:map(fun(JsonObj) ->
-                    Doc = couch_doc:from_json_obj_validate(JsonObj),
+                    Doc = couch_doc:from_json_obj_validate(JsonObj, DbName),
                     validate_attachment_names(Doc),
                     Doc
                 end, DocsArray),
@@ -486,14 +488,17 @@ db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) ->
 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, []),
+    DbName = couch_db:name(Db),
     case couch_httpd:qs_value(Req, "rev") of
     undefined ->
         update_doc(Req, Db, DocId,
-                couch_doc_from_req(Req, DocId, {[{<<"_deleted">>,true}]}));
+                couch_doc_from_req(Req, DocId, {[{<<"_deleted">>,true}]},
+                    DbName));
     Rev ->
         update_doc(Req, Db, DocId,
                 couch_doc_from_req(Req, DocId,
-                    {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}))
+                    {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]},
+                    DbName))
     end;
 
 db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) ->
@@ -546,7 +551,8 @@ db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) ->
 
 db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
     couch_httpd:validate_referer(Req),
-    couch_doc:validate_docid(DocId),
+    DbName = couch_db:name(Db),
+    couch_doc:validate_docid(DocId, DbName),
     couch_httpd:validate_ctype(Req, "multipart/form-data"),
     Form = couch_httpd:parse_form(Req),
     case couch_util:get_value("_doc", Form) of
@@ -554,7 +560,7 @@ db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
         Rev = couch_doc:parse_rev(couch_util:get_value("_rev", Form)),
         {ok, [{ok, Doc}]} = couch_db:open_doc_revs(Db, DocId, [Rev], []);
     Json ->
-        Doc = couch_doc_from_req(Req, DocId, ?JSON_DECODE(Json))
+        Doc = couch_doc_from_req(Req, DocId, ?JSON_DECODE(Json), DbName)
     end,
     UpdatedAtts = [
         couch_att:new([
@@ -580,14 +586,15 @@ db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
     update_doc(Req, Db, DocId, NewDoc);
 
 db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
-    couch_doc:validate_docid(DocId),
+    DbName = couch_db:name(Db),
+    couch_doc:validate_docid(DocId, DbName),
 
     case couch_util:to_list(couch_httpd:header_value(Req, "Content-Type")) of
     ("multipart/related;" ++ _) = ContentType ->
         couch_httpd:check_max_request_length(Req),
         {ok, Doc0, WaitFun, Parser} = couch_doc:doc_from_multi_part_stream(
             ContentType, fun() -> receive_request_data(Req) end),
-        Doc = couch_doc_from_req(Req, DocId, Doc0),
+        Doc = couch_doc_from_req(Req, DocId, Doc0, DbName),
         try
             Result = update_doc(Req, Db, DocId, Doc),
             WaitFun(),
@@ -599,7 +606,7 @@ db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
         end;
     _Else ->
         Body = couch_httpd:json_body(Req),
-        Doc = couch_doc_from_req(Req, DocId, Body),
+        Doc = couch_doc_from_req(Req, DocId, Body, DbName),
         update_doc(Req, Db, DocId, Doc)
     end;
 
@@ -783,7 +790,7 @@ update_doc(Req, Db, DocId, #doc{deleted=Deleted}=Doc, Headers, UpdateType) ->
                 {rev, NewRevStr}]})
     end.
 
-couch_doc_from_req(Req, DocId, #doc{revs=Revs}=Doc) ->
+couch_doc_from_req(Req, DocId, #doc{revs=Revs}=Doc, _) ->
     validate_attachment_names(Doc),
     Rev = case couch_httpd:qs_value(Req, "rev") of
     undefined ->
@@ -810,8 +817,9 @@ couch_doc_from_req(Req, DocId, #doc{revs=Revs}=Doc) ->
         end
     end,
     Doc#doc{id=DocId, revs=Revs2};
-couch_doc_from_req(Req, DocId, Json) ->
-    couch_doc_from_req(Req, DocId, couch_doc:from_json_obj_validate(Json)).
+couch_doc_from_req(Req, DocId, Json, DbName) ->
+    couch_doc_from_req(Req, DocId,
+        couch_doc:from_json_obj_validate(Json, DbName), DbName).
 
 % Useful for debugging
 % couch_doc_open(Db, DocId) ->
@@ -1019,7 +1027,7 @@ db_attachment_req(#httpd{method=Method,mochi_req=MochiReq}=Req, Db, DocId, FileN
                 % check for the existence of the doc to handle the 404 case.
                 couch_doc_open(Db, DocId, nil, [])
             end,
-            couch_doc:validate_docid(DocId),
+            couch_doc:validate_docid(DocId, couch_db:name(Db)),
             #doc{id=DocId};
         Rev ->
             case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
diff --git a/src/couch/test/couch_doc_json_tests.erl b/src/couch/test/couch_doc_json_tests.erl
index ce099d1..bcff064 100644
--- a/src/couch/test/couch_doc_json_tests.erl
+++ b/src/couch/test/couch_doc_json_tests.erl
@@ -171,6 +171,45 @@ from_json_success_cases() ->
         end,
         Cases).
 
+from_json_with_db_name_success_cases() ->
+    Cases = [
+        {
+            {[]},
+            <<"_dbs">>,
+            #doc{},
+            "DbName _dbs is acceptable with no docid"
+        },
+        {
+            {[{<<"_id">>, <<"zing!">>}]},
+            <<"_dbs">>,
+            #doc{id = <<"zing!">>},
+            "DbName _dbs is acceptable with a normal docid"
+        },
+        {
+            {[{<<"_id">>, <<"_users">>}]},
+            <<"_dbs">>,
+            #doc{id = <<"_users">>},
+            "_dbs/_users is acceptable"
+        },
+        {
+            {[{<<"_id">>, <<"_replicator">>}]},
+            <<"_dbs">>,
+            #doc{id = <<"_replicator">>},
+            "_dbs/_replicator is acceptable"
+        },
+        {
+            {[{<<"_id">>, <<"_global_changes">>}]},
+            <<"_dbs">>,
+            #doc{id = <<"_global_changes">>},
+            "_dbs/_global_changes is acceptable"
+        }
+    ],
+    lists:map(
+        fun({EJson, DbName, Expect, Msg}) ->
+            {Msg, ?_assertMatch(Expect, couch_doc:from_json_obj_validate(EJson, DbName))}
+        end,
+        Cases).
+
 from_json_error_cases() ->
     Cases = [
         {
@@ -261,6 +300,38 @@ from_json_error_cases() ->
             end
     end, Cases).
 
+from_json_with_dbname_error_cases() ->
+    Cases = [
+        {
+            {[{<<"_id">>, <<"_random">>}]},
+            <<"_dbs">>,
+            {illegal_docid,
+             <<"Only reserved document ids may start with underscore.">>},
+            "Disallow non-system-DB underscore prefixed docids in _dbs database."
+        },
+        {
+            {[{<<"_id">>, <<"_random">>}]},
+            <<"foobar">>,
+            {illegal_docid,
+             <<"Only reserved document ids may start with underscore.">>},
+            "Disallow arbitrary underscore prefixed docids in regular database."
+        },
+        {
+            {[{<<"_id">>, <<"_users">>}]},
+            <<"foobar">>,
+            {illegal_docid,
+             <<"Only reserved document ids may start with underscore.">>},
+            "Disallow system-DB docid _users in regular database."
+        }
+    ],
+
+    lists:map(
+        fun({EJson, DbName, Expect, Msg}) ->
+            Error = (catch couch_doc:from_json_obj_validate(EJson, DbName)),
+            {Msg, ?_assertMatch(Expect, Error)}
+        end,
+        Cases).
+
 to_json_success_cases() ->
     Cases = [
         {
diff --git a/src/couch/test/couch_doc_tests.erl b/src/couch/test/couch_doc_tests.erl
index 5d0448a..cf41df6 100644
--- a/src/couch/test/couch_doc_tests.erl
+++ b/src/couch/test/couch_doc_tests.erl
@@ -29,7 +29,7 @@ doc_from_multi_part_stream_test() ->
     ContentType = "multipart/related;boundary=multipart_related_boundary~~~~~~~~~~~~~~~~~~~~",
     DataFun = fun() -> request(start) end,
 
-    mock_config_max_document_id_length(),
+    mock_config(),
     {ok, #doc{id = <<"doc0">>, atts = [_]}, _Fun, _Parser} =
         couch_doc:doc_from_multi_part_stream(ContentType, DataFun),
     meck:unload(config),
@@ -77,7 +77,7 @@ len_doc_to_multi_part_stream_test() ->
 validate_docid_test_() ->
     {setup,
         fun() ->
-            mock_config_max_document_id_length(),
+            mock_config(),
             ok = meck:new(couch_db_plugin, [passthrough]),
             meck:expect(couch_db_plugin, validate_docid, fun(_) -> false end)
         end,
@@ -90,6 +90,9 @@ validate_docid_test_() ->
             ?_assertEqual(ok, couch_doc:validate_docid(<<"_design/idx">>)),
             ?_assertEqual(ok, couch_doc:validate_docid(<<"_local/idx">>)),
             ?_assertEqual(ok, couch_doc:validate_docid(large_id(1024))),
+            ?_assertEqual(ok, couch_doc:validate_docid(<<"_users">>, <<"_dbs">>)),
+            ?_assertEqual(ok, couch_doc:validate_docid(<<"_replicator">>, <<"_dbs">>)),
+            ?_assertEqual(ok, couch_doc:validate_docid(<<"_global_changes">>, <<"_dbs">>)),
             ?_assertThrow({illegal_docid, _},
                 couch_doc:validate_docid(<<>>)),
             ?_assertThrow({illegal_docid, _},
@@ -103,7 +106,11 @@ validate_docid_test_() ->
             ?_assertThrow({illegal_docid, _},
                 couch_doc:validate_docid(<<"_local/">>)),
             ?_assertThrow({illegal_docid, _},
-                couch_doc:validate_docid(large_id(1025)))
+                couch_doc:validate_docid(large_id(1025))),
+            ?_assertThrow({illegal_docid, _},
+                couch_doc:validate_docid(<<"_users">>, <<"foo">>)),
+            ?_assertThrow({illegal_docid, _},
+                couch_doc:validate_docid(<<"_weeee">>, <<"_dbs">>))
         ]
     }.
 
@@ -127,11 +134,12 @@ collected() ->
     B = binary:replace(iolist_to_binary(get(data)), <<"\r\n">>, <<0>>, [global]),
     binary:split(B, [<<0>>], [global]).
 
-mock_config_max_document_id_length() ->
+mock_config() ->
     ok = meck:new(config, [passthrough]),
     meck:expect(config, get,
         fun("couchdb", "max_document_id_length", "infinity") -> "1024";
            ("couchdb", "max_attachment_size", "infinity") -> "infinity";
+           ("mem3", "shards_db", "_dbs") -> "_dbs";
             (Key, Val, Default) -> meck:passthrough([Key, Val, Default])
         end
     ).

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.