Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 3B1CF4D1A for ; Wed, 25 May 2011 19:01:26 +0000 (UTC) Received: (qmail 67260 invoked by uid 500); 25 May 2011 19:01:26 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 67230 invoked by uid 500); 25 May 2011 19:01:26 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 67222 invoked by uid 99); 25 May 2011 19:01:26 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 25 May 2011 19:01:26 +0000 X-ASF-Spam-Status: No, hits=-1996.4 required=5.0 tests=ALL_TRUSTED,FS_REPLICA X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 25 May 2011 19:01:24 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 3FCE323889C5; Wed, 25 May 2011 19:01:04 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1127634 - in /couchdb/branches/1.1.x: share/www/script/test/replicator_db.js src/couchdb/couch_js_functions.hrl src/couchdb/couch_rep.erl src/couchdb/couch_replication_manager.erl Date: Wed, 25 May 2011 19:01:04 -0000 To: commits@couchdb.apache.org From: fdmanana@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20110525190104.3FCE323889C5@eris.apache.org> Author: fdmanana Date: Wed May 25 19:01:03 2011 New Revision: 1127634 URL: http://svn.apache.org/viewvc?rev=1127634&view=rev Log: Backported revision 1127632 from trunk 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/branches/1.1.x/share/www/script/test/replicator_db.js couchdb/branches/1.1.x/src/couchdb/couch_js_functions.hrl couchdb/branches/1.1.x/src/couchdb/couch_rep.erl couchdb/branches/1.1.x/src/couchdb/couch_replication_manager.erl Modified: couchdb/branches/1.1.x/share/www/script/test/replicator_db.js URL: http://svn.apache.org/viewvc/couchdb/branches/1.1.x/share/www/script/test/replicator_db.js?rev=1127634&r1=1127633&r2=1127634&view=diff ============================================================================== --- couchdb/branches/1.1.x/share/www/script/test/replicator_db.js (original) +++ couchdb/branches/1.1.x/share/www/script/test/replicator_db.js Wed May 25 19:01:03 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/branches/1.1.x/src/couchdb/couch_js_functions.hrl URL: http://svn.apache.org/viewvc/couchdb/branches/1.1.x/src/couchdb/couch_js_functions.hrl?rev=1127634&r1=1127633&r2=1127634&view=diff ============================================================================== --- couchdb/branches/1.1.x/src/couchdb/couch_js_functions.hrl (original) +++ couchdb/branches/1.1.x/src/couchdb/couch_js_functions.hrl Wed May 25 19:01:03 2011 @@ -182,12 +182,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)) { @@ -204,24 +198,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/branches/1.1.x/src/couchdb/couch_rep.erl URL: http://svn.apache.org/viewvc/couchdb/branches/1.1.x/src/couchdb/couch_rep.erl?rev=1127634&r1=1127633&r2=1127634&view=diff ============================================================================== --- couchdb/branches/1.1.x/src/couchdb/couch_rep.erl (original) +++ couchdb/branches/1.1.x/src/couchdb/couch_rep.erl Wed May 25 19:01:03 2011 @@ -899,13 +899,14 @@ update_rep_doc(RepDb, #doc{body = {RepDo RepDocBody, KVs ), - % might not succeed - when the replication doc is deleted right - % before this update (not an error) - 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) + couch_db:update_doc(RepDb, RepDoc#doc{body = {NewRepDocBody}}, []) + end. % RFC3339 timestamps. % Note: doesn't include the time seconds fraction (RFC3339 says it's optional). Modified: couchdb/branches/1.1.x/src/couchdb/couch_replication_manager.erl URL: http://svn.apache.org/viewvc/couchdb/branches/1.1.x/src/couchdb/couch_replication_manager.erl?rev=1127634&r1=1127633&r2=1127634&view=diff ============================================================================== --- couchdb/branches/1.1.x/src/couchdb/couch_replication_manager.erl (original) +++ couchdb/branches/1.1.x/src/couchdb/couch_replication_manager.erl Wed May 25 19:01:03 2011 @@ -253,7 +253,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), @@ -307,6 +307,10 @@ start_replication(Server, {RepProps} = R ok = gen_server:call(Server, {triggered, RepId}, infinity), couch_rep:get_result(Pid, RepId, RepDoc, UserCtx); Error -> + couch_rep:update_rep_doc( + RepDoc, + [{<<"_replication_state">>, <<"error">>}, + {<<"_replication_id">>, ?l2b(element(1, RepId))}]), keep_retrying( Server, RepId, RepDoc, UserCtx, Error, ?INITIAL_WAIT, MaxRetries) end.