You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by fd...@apache.org on 2011/05/25 20:59:24 UTC

svn commit: r1127632 - in /couchdb/trunk: share/www/script/test/replicator_db.js src/couchdb/couch_js_functions.hrl src/couchdb/couch_replication_manager.erl

Author: fdmanana
Date: Wed May 25 18:59:23 2011
New Revision: 1127632

URL: http://svn.apache.org/viewvc?rev=1127632&view=rev
Log:
Force non admins to supply a user_ctx in replication documents

This is to prevent users deleting replication documents added by other users
and to make it clear who triggers which replications.

Modified:
    couchdb/trunk/share/www/script/test/replicator_db.js
    couchdb/trunk/src/couchdb/couch_js_functions.hrl
    couchdb/trunk/src/couchdb/couch_replication_manager.erl

Modified: couchdb/trunk/share/www/script/test/replicator_db.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/replicator_db.js?rev=1127632&r1=1127631&r2=1127632&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/replicator_db.js (original)
+++ couchdb/trunk/share/www/script/test/replicator_db.js Wed May 25 18:59:23 2011
@@ -186,7 +186,10 @@ couchTests.replicator_db = function(debu
       _id: "foo_cont_rep_doc",
       source: "http://" + host + "/" + dbA.name,
       target: dbB.name,
-      continuous: true
+      continuous: true,
+      user_ctx: {
+        roles: ["_admin"]
+      }
     };
 
     T(repDb.save(repDoc).ok);
@@ -220,10 +223,8 @@ couchTests.replicator_db = function(debu
     T(typeof repDoc1._replication_state_time === "string");
     T(typeof repDoc1._replication_id  === "string");
 
-    // add a design doc to source, it will be replicated to target
-    // when the "user_ctx" property is not defined in the replication doc,
-    // the replication will be done under an _admin context, therefore
-    // design docs will be replicated
+    // Design documents are only replicated to local targets if the respective
+    // replication document has a user_ctx filed with the "_admin" role in it.
     var ddoc = {
       _id: "_design/foobar",
       language: "javascript"
@@ -303,8 +304,7 @@ couchTests.replicator_db = function(debu
     T(copy === null);
 
     copy = dbB.open("_design/mydesign");
-    T(copy !== null);
-    T(copy.language === "javascript");
+    T(copy === null);
   }
 
 
@@ -713,6 +713,225 @@ couchTests.replicator_db = function(debu
   }
 
 
+  function test_user_ctx_validation() {
+    populate_db(dbA, docs1);
+    populate_db(dbB, []);
+    populate_db(usersDb, []);
+
+    var joeUserDoc = CouchDB.prepareUserDoc({
+      name: "joe",
+      roles: ["erlanger", "bar"]
+    }, "erly");
+    var fdmananaUserDoc = CouchDB.prepareUserDoc({
+      name: "fdmanana",
+      roles: ["a", "b", "c"]
+    }, "qwerty");
+
+    TEquals(true, usersDb.save(joeUserDoc).ok);
+    TEquals(true, usersDb.save(fdmananaUserDoc).ok);
+
+    T(dbB.setSecObj({
+      admins: {
+        names: [],
+        roles: ["god"]
+      },
+      readers: {
+        names: [],
+        roles: ["foo"]
+      }
+    }).ok);
+
+    TEquals(true, CouchDB.login("joe", "erly").ok);
+    TEquals("joe", CouchDB.session().userCtx.name);
+    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));
+
+    var repDoc = {
+      _id: "foo_rep",
+      source: CouchDB.protocol + host + "/" + dbA.name,
+      target: dbB.name
+    };
+
+    try {
+      repDb.save(repDoc);
+      T(false, "Should have failed, user_ctx missing.");
+    } catch (x) {
+      TEquals("forbidden", x.error);
+    }
+
+    repDoc.user_ctx = {
+      name: "john",
+      roles: ["erlanger"]
+    };
+
+    try {
+      repDb.save(repDoc);
+      T(false, "Should have failed, wrong user_ctx.name.");
+    } catch (x) {
+      TEquals("forbidden", x.error);
+    }
+
+    repDoc.user_ctx = {
+      name: "joe",
+      roles: ["bar", "god", "erlanger"]
+    };
+
+    try {
+      repDb.save(repDoc);
+      T(false, "Should have failed, a bad role in user_ctx.roles.");
+    } catch (x) {
+      TEquals("forbidden", x.error);
+    }
+
+    // user_ctx.roles might contain only a subset of the user's roles
+    repDoc.user_ctx = {
+      name: "joe",
+      roles: ["erlanger"]
+    };
+
+    TEquals(true, repDb.save(repDoc).ok);
+    CouchDB.logout();
+
+    waitForRep(repDb, repDoc, "error");
+    var repDoc1 = repDb.open(repDoc._id);
+    T(repDoc1 !== null);
+    TEquals(repDoc.source, repDoc1.source);
+    TEquals(repDoc.target, repDoc1.target);
+    TEquals("error", repDoc1._replication_state);
+    TEquals("string", typeof repDoc1._replication_id);
+    TEquals("string", typeof repDoc1._replication_state_time);
+
+    TEquals(true, CouchDB.login("fdmanana", "qwerty").ok);
+    TEquals("fdmanana", CouchDB.session().userCtx.name);
+    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));
+
+    try {
+      T(repDb.deleteDoc(repDoc1).ok);
+      T(false, "Shouldn't be able to delete replication document.");
+    } catch (x) {
+      TEquals("forbidden", x.error);
+    }
+
+    CouchDB.logout();
+    TEquals(true, CouchDB.login("joe", "erly").ok);
+    TEquals("joe", CouchDB.session().userCtx.name);
+    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));
+
+    T(repDb.deleteDoc(repDoc1).ok);
+    CouchDB.logout();
+
+    for (var i = 0; i < docs1.length; i++) {
+      var doc = docs1[i];
+      var copy = dbB.open(doc._id);
+
+      TEquals(null, copy);
+    }
+
+    T(dbB.setSecObj({
+      admins: {
+        names: [],
+        roles: ["god", "erlanger"]
+      },
+      readers: {
+        names: [],
+        roles: ["foo"]
+      }
+    }).ok);
+
+    TEquals(true, CouchDB.login("joe", "erly").ok);
+    TEquals("joe", CouchDB.session().userCtx.name);
+    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));
+
+    repDoc = {
+      _id: "foo_rep_2",
+      source: CouchDB.protocol + host + "/" + dbA.name,
+      target: dbB.name,
+      user_ctx: {
+        name: "joe",
+        roles: ["erlanger"]
+      }
+    };
+
+    TEquals(true, repDb.save(repDoc).ok);
+    CouchDB.logout();
+
+    waitForRep(repDb, repDoc, "complete");
+    repDoc1 = repDb.open(repDoc._id);
+    T(repDoc1 !== null);
+    TEquals(repDoc.source, repDoc1.source);
+    TEquals(repDoc.target, repDoc1.target);
+    TEquals("completed", repDoc1._replication_state);
+    TEquals("string", typeof repDoc1._replication_id);
+    TEquals("string", typeof repDoc1._replication_state_time);
+
+    for (var i = 0; i < docs1.length; i++) {
+      var doc = docs1[i];
+      var copy = dbB.open(doc._id);
+
+      T(copy !== null);
+      TEquals(doc.value, copy.value);
+    }
+
+    // Admins don't need to supply a user_ctx property in replication docs.
+    // If they do not, the implicit user_ctx "user_ctx": {name: null, roles: []}
+    // is used, meaning that design documents will not be replicated into
+    // local targets
+    T(dbB.setSecObj({
+      admins: {
+        names: [],
+        roles: []
+      },
+      readers: {
+        names: [],
+        roles: []
+      }
+    }).ok);
+
+    var ddoc = { _id: "_design/foo" };
+    TEquals(true, dbA.save(ddoc).ok);
+
+    repDoc = {
+      _id: "foo_rep_3",
+      source: CouchDB.protocol + host + "/" + dbA.name,
+      target: dbB.name
+    };
+
+    TEquals(true, repDb.save(repDoc).ok);
+    waitForRep(repDb, repDoc, "complete");
+    repDoc1 = repDb.open(repDoc._id);
+    T(repDoc1 !== null);
+    TEquals(repDoc.source, repDoc1.source);
+    TEquals(repDoc.target, repDoc1.target);
+    TEquals("completed", repDoc1._replication_state);
+    TEquals("string", typeof repDoc1._replication_id);
+    TEquals("string", typeof repDoc1._replication_state_time);
+
+    var ddoc_copy = dbB.open(ddoc._id);
+    T(ddoc_copy === null);
+
+    repDoc = {
+      _id: "foo_rep_4",
+      source: CouchDB.protocol + host + "/" + dbA.name,
+      target: dbB.name,
+      user_ctx: {
+        roles: ["_admin"]
+      }
+    };
+
+    TEquals(true, repDb.save(repDoc).ok);
+    waitForRep(repDb, repDoc, "complete");
+    repDoc1 = repDb.open(repDoc._id);
+    T(repDoc1 !== null);
+    TEquals(repDoc.source, repDoc1.source);
+    TEquals(repDoc.target, repDoc1.target);
+    TEquals("completed", repDoc1._replication_state);
+    TEquals("string", typeof repDoc1._replication_id);
+    TEquals("string", typeof repDoc1._replication_state_time);
+
+    ddoc_copy = dbB.open(ddoc._id);
+    T(ddoc_copy !== null);
+  }
+
+
   function rep_doc_with_bad_rep_id() {
     populate_db(dbA, docs1);
     populate_db(dbB, []);
@@ -1111,6 +1330,11 @@ couchTests.replicator_db = function(debu
       value: usersDb.name
     }
   ]);
+
+  repDb.deleteDb();
+  restartServer();
+  run_on_modified_server(server_config_2, test_user_ctx_validation);
+
   repDb.deleteDb();
   restartServer();
   run_on_modified_server(server_config_2, test_replication_credentials_delegation);

Modified: couchdb/trunk/src/couchdb/couch_js_functions.hrl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_js_functions.hrl?rev=1127632&r1=1127631&r2=1127632&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_js_functions.hrl (original)
+++ couchdb/trunk/src/couchdb/couch_js_functions.hrl Wed May 25 18:59:23 2011
@@ -191,12 +191,6 @@
             }
 
             if (newDoc.user_ctx) {
-                if (!isAdmin) {
-                    reportError('Delegated replications (use of the ' +
-                        '`user_ctx\\' property) can only be triggered by ' +
-                        'administrators.');
-                }
-
                 var user_ctx = newDoc.user_ctx;
 
                 if ((typeof user_ctx !== 'object') || (user_ctx === null)) {
@@ -213,24 +207,40 @@
                         'non-empty string or null.');
                 }
 
+                if (!isAdmin && (user_ctx.name !== userCtx.name)) {
+                    reportError('The given `user_ctx.name\\' is not valid');
+                }
+
                 if (user_ctx.roles && !isArray(user_ctx.roles)) {
                     reportError('The `user_ctx.roles\\' property must be ' +
                         'an array of strings.');
                 }
 
-                if (user_ctx.roles) {
+                if (!isAdmin && user_ctx.roles) {
                     for (var i = 0; i < user_ctx.roles.length; i++) {
                         var role = user_ctx.roles[i];
 
                         if (typeof role !== 'string' || role.length === 0) {
                             reportError('Roles must be non-empty strings.');
                         }
-                        if (role[0] === '_') {
-                            reportError('System roles (starting with an ' +
-                                'underscore) are not allowed.');
+                        if (userCtx.roles.indexOf(role) === -1) {
+                            reportError('Invalid role (`' + role +
+                                '\\') in the `user_ctx\\'');
                         }
                     }
                 }
+            } else {
+                if (!isAdmin) {
+                    reportError('The `user_ctx\\' property is missing (it is ' +
+                       'optional for admins only).');
+                }
+            }
+        } else {
+            if (!isAdmin) {
+                if (!oldDoc.user_ctx || (oldDoc.user_ctx.name !== userCtx.name)) {
+                    reportError('Replication documents can only be deleted by ' +
+                        'admins or by the users who created them.');
+                }
             }
         }
     }

Modified: couchdb/trunk/src/couchdb/couch_replication_manager.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_replication_manager.erl?rev=1127632&r1=1127631&r2=1127632&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_replication_manager.erl (original)
+++ couchdb/trunk/src/couchdb/couch_replication_manager.erl Wed May 25 18:59:23 2011
@@ -85,13 +85,15 @@ replication_completed(#rep{id = RepId}) 
     end.
 
 
-replication_error(#rep{id = RepId}, Error) ->
+replication_error(#rep{id = {BaseId, _} = RepId}, Error) ->
     case rep_state(RepId) of
     nil ->
         ok;
     #rep_state{rep = #rep{doc_id = DocId}} ->
         % TODO: maybe add error reason to replication document
-        update_rep_doc(DocId, [{<<"_replication_state">>, <<"error">>}]),
+        update_rep_doc(DocId, [
+            {<<"_replication_state">>, <<"error">>},
+            {<<"_replication_id">>, ?l2b(BaseId)}]),
         ok = gen_server:call(?MODULE, {rep_error, RepId, Error}, infinity)
     end.
 
@@ -310,7 +312,7 @@ process_update(State, {Change}) ->
 rep_user_ctx({RepDoc}) ->
     case get_value(<<"user_ctx">>, RepDoc) of
     undefined ->
-        #user_ctx{roles = [<<"_admin">>]};
+        #user_ctx{};
     {UserCtx} ->
         #user_ctx{
             name = get_value(<<"name">>, UserCtx, null),
@@ -482,9 +484,14 @@ update_rep_doc(RepDb, #doc{body = {RepDo
                 lists:keystore(K, 1, Body, KV)
         end,
         RepDocBody, KVs),
-    % Might not succeed - when the replication doc is deleted right
-    % before this update (not an error, ignore).
-    couch_db:update_doc(RepDb, RepDoc#doc{body = {NewRepDocBody}}, []).
+    case NewRepDocBody of
+    RepDocBody ->
+        ok;
+    _ ->
+        % Might not succeed - when the replication doc is deleted right
+        % before this update (not an error, ignore).
+        couch_db:update_doc(RepDb, RepDoc#doc{body = {NewRepDocBody}}, [])
+    end.
 
 
 % RFC3339 timestamps.