mirror of https://github.com/mongodb/mongo
241 lines
8.7 KiB
JavaScript
241 lines
8.7 KiB
JavaScript
// Tests rollback of auth data in replica sets.
|
|
// This test creates a user and then does two different sets of updates to that user's privileges
|
|
// using the replSetTest command to trigger a rollback and verify that at the end the access control
|
|
// data is rolled back correctly and the user only has access to the expected collections.
|
|
//
|
|
// If all data-bearing nodes in a replica set are using an ephemeral storage engine, the set will
|
|
// not be able to survive a scenario where all data-bearing nodes are down simultaneously. In such a
|
|
// scenario, none of the members will have any data, and upon restart will each look for a member to
|
|
// inital sync from, so no primary will be elected. This test induces such a scenario, so cannot be
|
|
// run on ephemeral storage engines.
|
|
// @tags: [requires_persistence]
|
|
|
|
import {ReplSetTest} from "jstests/libs/replsettest.js";
|
|
|
|
// Multiple users cannot be authenticated on one connection within a session.
|
|
TestData.disableImplicitSessions = true;
|
|
|
|
// helper function for verifying contents at the end of the test
|
|
function checkFinalResults(db) {
|
|
assert.commandWorked(db.runCommand({dbStats: 1}));
|
|
assert.commandFailedWithCode(db.runCommand({collStats: "foo"}), authzErrorCode);
|
|
assert.commandFailedWithCode(db.runCommand({collStats: "bar"}), authzErrorCode);
|
|
assert.commandWorked(db.runCommand({collStats: "baz"}));
|
|
assert.commandWorked(db.runCommand({collStats: "foobar"}));
|
|
}
|
|
|
|
const authzErrorCode = 13;
|
|
|
|
jsTestLog("Setting up replica set");
|
|
|
|
const name = "rollbackAuth";
|
|
const replTest = new ReplSetTest({name: name, nodes: 3, keyFile: "jstests/libs/key1"});
|
|
const nodes = replTest.nodeList();
|
|
const conns = replTest.startSet();
|
|
replTest.initiate(
|
|
{
|
|
"_id": "rollbackAuth",
|
|
"members": [
|
|
{"_id": 0, "host": nodes[0], "priority": 3},
|
|
{"_id": 1, "host": nodes[1]},
|
|
{"_id": 2, "host": nodes[2], arbiterOnly: true},
|
|
],
|
|
},
|
|
null,
|
|
{initiateWithDefaultElectionTimeout: true},
|
|
);
|
|
|
|
// Make sure we have a primary
|
|
replTest.waitForState(replTest.nodes[0], ReplSetTest.State.PRIMARY);
|
|
const primary = replTest.getPrimary();
|
|
const a_conn = conns[0];
|
|
const b_conn = conns[1];
|
|
a_conn.setSecondaryOk();
|
|
b_conn.setSecondaryOk();
|
|
const A_admin = a_conn.getDB("admin");
|
|
const B_admin = b_conn.getDB("admin");
|
|
const A_test = a_conn.getDB("test");
|
|
const B_test = b_conn.getDB("test");
|
|
assert.eq(primary, conns[0], "conns[0] assumed to be primary");
|
|
assert.eq(a_conn, primary);
|
|
|
|
// Make sure we have an arbiter
|
|
assert.soon(function () {
|
|
const res = conns[2].getDB("admin").runCommand({replSetGetStatus: 1});
|
|
return res.myState == 7;
|
|
}, "Arbiter failed to initialize.");
|
|
|
|
jsTestLog("Creating initial data");
|
|
|
|
// Create collections that will be used in test
|
|
A_admin.createUser({user: "admin", pwd: "pwd", roles: ["root"]});
|
|
A_admin.auth("admin", "pwd");
|
|
|
|
// Set up user admin user
|
|
A_admin.createUser({user: "userAdmin", pwd: "pwd", roles: ["userAdminAnyDatabase"]});
|
|
|
|
A_test.foo.insert({a: 1});
|
|
A_test.bar.insert({a: 1});
|
|
A_test.baz.insert({a: 1});
|
|
A_test.foobar.insert({a: 1});
|
|
A_admin.logout();
|
|
|
|
assert(A_admin.auth("userAdmin", "pwd"));
|
|
|
|
// Give replication time to catch up.
|
|
assert.soon(function () {
|
|
return B_admin.auth("userAdmin", "pwd");
|
|
});
|
|
|
|
// Create a basic user and role
|
|
A_admin.createRole({
|
|
role: "replStatusRole", // To make awaitReplication() work
|
|
roles: [],
|
|
privileges: [
|
|
{resource: {cluster: true}, actions: ["replSetGetStatus"]},
|
|
{resource: {db: "local", collection: ""}, actions: ["find"]},
|
|
{resource: {db: "local", collection: "system.replset"}, actions: ["find"]},
|
|
],
|
|
});
|
|
A_test.createRole({
|
|
role: "myRole",
|
|
roles: [],
|
|
privileges: [{resource: {db: "test", collection: ""}, actions: ["dbStats"]}],
|
|
});
|
|
A_test.createUser({user: "spencer", pwd: "pwd", roles: ["myRole", {role: "replStatusRole", db: "admin"}]});
|
|
|
|
A_admin.logout();
|
|
B_admin.logout();
|
|
|
|
assert(A_test.auth("spencer", "pwd"));
|
|
|
|
// wait for secondary to get this data
|
|
assert.soon(function () {
|
|
return B_test.auth("spencer", "pwd");
|
|
});
|
|
|
|
assert.commandWorked(A_test.runCommand({dbStats: 1}));
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "foo"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "bar"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "baz"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "foobar"}), authzErrorCode);
|
|
|
|
assert.commandWorked(B_test.runCommand({dbStats: 1}));
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "foo"}), authzErrorCode);
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "bar"}), authzErrorCode);
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "baz"}), authzErrorCode);
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "foobar"}), authzErrorCode);
|
|
|
|
replTest.awaitLastOpCommitted();
|
|
|
|
jsTestLog("Doing writes that will eventually be rolled back");
|
|
|
|
// down A and wait for B to become primary
|
|
A_test.logout();
|
|
replTest.stop(0);
|
|
assert.soon(function () {
|
|
try {
|
|
return B_admin.hello().isWritablePrimary;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}, "B didn't become primary");
|
|
printjson(assert.commandWorked(B_test.adminCommand("replSetGetStatus")));
|
|
B_test.logout();
|
|
|
|
// Modify the the user and role in a way that will be rolled back.
|
|
assert(B_admin.auth("admin", "pwd"));
|
|
B_test.grantPrivilegesToRole("myRole", [{resource: {db: "test", collection: "foo"}, actions: ["collStats"]}], {}); // Default write concern will wait for majority, which will time out.
|
|
B_test.createRole(
|
|
{
|
|
role: "temporaryRole",
|
|
roles: [],
|
|
privileges: [{resource: {db: "test", collection: "bar"}, actions: ["collStats"]}],
|
|
},
|
|
{},
|
|
); // Default write concern will wait for majority, which will time out.
|
|
B_test.grantRolesToUser("spencer", ["temporaryRole"], {}); // Default write concern will wait for majority, which will time out.
|
|
B_admin.logout();
|
|
|
|
assert(B_test.auth("spencer", "pwd"));
|
|
assert.commandWorked(B_test.runCommand({dbStats: 1}));
|
|
assert.commandWorked(B_test.runCommand({collStats: "foo"}));
|
|
assert.commandWorked(B_test.runCommand({collStats: "bar"}));
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "baz"}), authzErrorCode);
|
|
assert.commandFailedWithCode(B_test.runCommand({collStats: "foobar"}), authzErrorCode);
|
|
B_test.logout();
|
|
|
|
// down B, bring A back up, then wait for A to become primary
|
|
// insert new data into A so that B will need to rollback when it reconnects to A
|
|
replTest.stop(1);
|
|
|
|
replTest.restart(0);
|
|
assert.soon(function () {
|
|
try {
|
|
const helloResponse = A_admin.hello();
|
|
jsTestLog("A hello response: " + tojson(helloResponse));
|
|
return helloResponse.isWritablePrimary;
|
|
} catch (e) {
|
|
jsTestLog("hello() threw an exception when waiting for A to become primary: " + tojson(e));
|
|
return false;
|
|
}
|
|
}, "A didn't become primary");
|
|
|
|
// A should not have the new data as it was down
|
|
assert(A_test.auth("spencer", "pwd"));
|
|
assert.commandWorked(A_test.runCommand({dbStats: 1}));
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "foo"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "bar"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "baz"}), authzErrorCode);
|
|
assert.commandFailedWithCode(A_test.runCommand({collStats: "foobar"}), authzErrorCode);
|
|
A_test.logout();
|
|
|
|
jsTestLog("Doing writes that should persist after the rollback");
|
|
// Modify the user and role in a way that will persist.
|
|
A_admin.auth("userAdmin", "pwd");
|
|
// Default write concern will wait for majority, which would time out
|
|
// so we override it with an empty write concern
|
|
A_test.grantPrivilegesToRole("myRole", [{resource: {db: "test", collection: "baz"}, actions: ["collStats"]}], {});
|
|
|
|
A_test.createRole(
|
|
{
|
|
role: "persistentRole",
|
|
roles: [],
|
|
privileges: [{resource: {db: "test", collection: "foobar"}, actions: ["collStats"]}],
|
|
},
|
|
{},
|
|
);
|
|
A_test.grantRolesToUser("spencer", ["persistentRole"], {});
|
|
A_admin.logout();
|
|
|
|
A_test.auth("spencer", "pwd");
|
|
|
|
// A has the data we just wrote, but not what B wrote before
|
|
checkFinalResults(A_test);
|
|
|
|
jsTestLog("Triggering rollback");
|
|
|
|
// bring B back in contact with A
|
|
// as A is primary, B will roll back and then catch up
|
|
replTest.restart(1);
|
|
assert.soonNoExcept(function () {
|
|
authutil.asCluster(replTest.nodes, "jstests/libs/key1", function () {
|
|
replTest.awaitReplication();
|
|
});
|
|
|
|
return B_test.auth("spencer", "pwd");
|
|
});
|
|
// Now both A and B should agree
|
|
checkFinalResults(A_test);
|
|
checkFinalResults(B_test);
|
|
|
|
A_test.logout();
|
|
|
|
// Verify data consistency between nodes.
|
|
authutil.asCluster(replTest.nodes, "jstests/libs/key1", function () {
|
|
replTest.checkOplogs();
|
|
});
|
|
|
|
// DB hash check is done in stopSet.
|
|
replTest.stopSet();
|