Permalink
Browse files

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.

Change-Id: I5877b93bdd12e6134cbb8c67911bd16039047376
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@1127632 13f79535-47bb-0310-9956-ffa450edef68
Reviewed-on: http://review.membase.org/6558
Reviewed-by: Filipe David Borba Manana <fdmanana@gmail.com>
Tested-by: Filipe David Borba Manana <fdmanana@gmail.com>
  • Loading branch information...
1 parent ed3a1cc commit f8531301d929e6e35ef0ed6fd9414b84dbd3224b @fdmanana fdmanana committed with dustin May 25, 2011
Showing with 264 additions and 23 deletions.
  1. +231 −7 share/www/script/test/replicator_db.js
  2. +20 −10 src/couchdb/couch_js_functions.hrl
  3. +13 −6 src/couchdb/couch_replication_manager.erl
@@ -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);
@@ -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.');
+ }
}
}
}
@@ -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 = {RepDocBody}} = RepDoc, KVs) ->
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.

0 comments on commit f853130

Please sign in to comment.