diff options
| author | Filipe David Borba Manana <fdmanana@apache.org> | 2011-05-25 19:01:03 +0000 | 
|---|---|---|
| committer | Filipe David Borba Manana <fdmanana@apache.org> | 2011-05-25 19:01:03 +0000 | 
| commit | 172a751ce84a46d2f121a1c57f6d5554447c7bee (patch) | |
| tree | 1784dd3c5a6feff40b10776b1070c94722be3970 | |
| parent | d952ac01cb4cd4ae5ceb0c8cc079acf595ff9747 (diff) | |
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.
git-svn-id: https://svn.apache.org/repos/asf/couchdb/branches/1.1.x@1127634 13f79535-47bb-0310-9956-ffa450edef68
| -rw-r--r-- | share/www/script/test/replicator_db.js | 238 | ||||
| -rw-r--r-- | src/couchdb/couch_js_functions.hrl | 30 | ||||
| -rw-r--r-- | src/couchdb/couch_rep.erl | 15 | ||||
| -rw-r--r-- | src/couchdb/couch_replication_manager.erl | 6 | 
4 files changed, 264 insertions, 25 deletions
| diff --git a/share/www/script/test/replicator_db.js b/share/www/script/test/replicator_db.js index c28e067d..4434124e 100644 --- a/share/www/script/test/replicator_db.js +++ b/share/www/script/test/replicator_db.js @@ -186,7 +186,10 @@ couchTests.replicator_db = function(debug) {        _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(debug) {      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(debug) {      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(debug) {    } +  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(debug) {        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); diff --git a/src/couchdb/couch_js_functions.hrl b/src/couchdb/couch_js_functions.hrl index 1e3ed4e9..d07eead5 100644 --- a/src/couchdb/couch_js_functions.hrl +++ b/src/couchdb/couch_js_functions.hrl @@ -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.'); +                }              }          }      } diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl index 49a82e5d..fd323f7f 100644 --- a/src/couchdb/couch_rep.erl +++ b/src/couchdb/couch_rep.erl @@ -899,13 +899,14 @@ update_rep_doc(RepDb, #doc{body = {RepDocBody}} = RepDoc, KVs) ->          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). diff --git a/src/couchdb/couch_replication_manager.erl b/src/couchdb/couch_replication_manager.erl index 6101c9c5..6537c8b2 100644 --- a/src/couchdb/couch_replication_manager.erl +++ b/src/couchdb/couch_replication_manager.erl @@ -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} = RepDoc, RepId, UserCtx, MaxRetries) ->          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. | 
