mongo/jstests/auth/mongos_cache_invalidation.js

329 lines
12 KiB
JavaScript

/**
* This tests that updates to user and role definitions made on one mongos propagate properly
* to other mongoses.
* @tags: [requires_sharding]
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
let authzErrorCode = 13;
let hasAuthzError = function (result) {
assert(result instanceof WriteCommandError);
assert.eq(authzErrorCode, result.code);
};
let st = new ShardingTest({
shards: 2,
config: 3,
mongos: [
{},
{setParameter: {userCacheInvalidationIntervalSecs: 5}},
{setParameter: {userCacheInvalidationIntervalSecs: 600}},
],
keyFile: "jstests/libs/key1",
});
st.s1.getDB("admin").createUser({user: "root", pwd: "pwd", roles: ["__system"]});
st.s1.getDB("admin").auth("root", "pwd");
let res = st.s1.getDB("admin").runCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 0});
assert.commandFailed(res, "Setting the invalidation interval to an disallowed value should fail");
res = st.s1.getDB("admin").runCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 100000});
assert.commandFailed(res, "Setting the invalidation interval to an disallowed value should fail");
res = st.s1.getDB("admin").runCommand({getParameter: 1, userCacheInvalidationIntervalSecs: 1});
assert.eq(5, res.userCacheInvalidationIntervalSecs);
assert.commandWorked(st.s1.getDB("test").foo.insert({a: 1})); // initial data
assert.commandWorked(st.s1.getDB("test").bar.insert({a: 1})); // initial data
st.s1.getDB("admin").createUser({user: "admin", pwd: "pwd", roles: ["userAdminAnyDatabase"]});
st.s1.getDB("admin").logout();
st.s0.getDB("admin").auth("admin", "pwd");
st.s0.getDB("admin").createRole({
role: "myRole",
roles: [],
privileges: [
{
resource: {cluster: true},
actions: ["invalidateUserCache", "getParameter", "setParameter", "getLog", "dbStats"],
},
],
});
st.s0.getDB("test").createUser({
user: "spencer",
pwd: "pwd",
roles: ["read", {role: "myRole", db: "admin"}, {role: "userAdminAnyDatabase", db: "admin"}],
});
st.s0.getDB("admin").logout();
const db1 = st.s0.getDB("test");
assert(db1.auth("spencer", "pwd"));
const db2 = st.s1.getDB("test");
assert(db2.auth("spencer", "pwd"));
const db3 = st.s2.getDB("test");
assert(db3.auth("spencer", "pwd"));
/**
* At this point we have 3 handles to the "test" database, each of which are on connections to
* different mongoses. "db1", "db2", and "db3" are all auth'd as spencer@test and will be used
* to verify that user and role data changes get propaged to their mongoses.
* "db2" is connected to a mongos with a 5 second user cache invalidation interval,
* while "db3" is connected to a mongos with a 10 minute cache invalidation interval.
*/
(function testChangingInvalidationInterval() {
jsTestLog("Test that changing the invalidation interval takes effect immediately");
assert.commandFailedWithCode(db3.bar.runCommand("drop"), authzErrorCode);
assert.eq(1, db3.bar.findOne().a);
db1.getSiblingDB("admin").grantPrivilegesToRole("myRole", [
{resource: {db: "test", collection: ""}, actions: ["dropCollection"]},
]);
// At first db3 should still think we're unauthorized because it hasn't invalidated it's cache.
assert.commandFailedWithCode(db3.bar.runCommand("drop"), authzErrorCode);
// Changing the value of the invalidation interval should make it invalidate its cache quickly.
assert.commandWorked(db3.adminCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 1}));
sleep(2000);
assert.commandWorked(db3.bar.runCommand("drop"));
assert.eq(0, db3.bar.count());
// Set the invalidation interval back for the rest of the tests
db3.adminCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 600});
})();
(function testGrantingPrivileges() {
jsTestLog("Testing propagation of granting privileges");
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
hasAuthzError(db2.foo.update({}, {$inc: {a: 1}}));
hasAuthzError(db3.foo.update({}, {$inc: {a: 1}}));
assert.eq(1, db1.foo.findOne().a);
assert.eq(1, db2.foo.findOne().a);
assert.eq(1, db3.foo.findOne().a);
db1.getSiblingDB("admin").grantPrivilegesToRole("myRole", [
{resource: {db: "test", collection: ""}, actions: ["update"]},
]);
// s0/db1 should update its cache instantly
assert.commandWorked(db1.foo.update({}, {$inc: {a: 1}}));
assert.eq(2, db1.foo.findOne().a);
// s1/db2 should update its cache in 10 seconds.
sleep(10000);
assert.soon(
function () {
let res = db2.foo.update({}, {$inc: {a: 1}});
if (res instanceof WriteCommandError) {
return false;
}
return db2.foo.findOne().a == 3;
},
"Mongos did not update its user cache after 10 seconds",
5 * 1000 /* Additional 5 seconds of buffer in case of slow update */,
);
// We manually invalidate the cache on s2/db3.
db3.adminCommand("invalidateUserCache");
assert.commandWorked(db3.foo.update({}, {$inc: {a: 1}}));
assert.eq(4, db3.foo.findOne().a);
})();
(function testRevokingPrivileges() {
jsTestLog("Testing propagation of revoking privileges");
db1.getSiblingDB("admin").revokePrivilegesFromRole("myRole", [
{resource: {db: "test", collection: ""}, actions: ["update"]},
]);
// s0/db1 should update its cache instantly
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
jsTest.log("Beginning wait for s1/db2 cache update.");
// s1/db2 should update its cache in 10 seconds.
sleep(10000);
assert.soon(
function () {
let res = db2.foo.update({}, {$inc: {a: 1}});
return res instanceof WriteCommandError && res.code == authzErrorCode;
},
"Mongos did not update its user cache after 10 seconds",
5 * 1000 /* Additional 5 seconds of buffer in case of slow update */,
);
// We manually invalidate the cache on s1/db3.
db3.adminCommand("invalidateUserCache");
hasAuthzError(db3.foo.update({}, {$inc: {a: 1}}));
})();
/**
* Test we do not flush the cache and generate a new userCacheGeneration if there are no user
* changes.
*/
(function testKeepingCacheIfNoChange() {
jsTestLog("Testing cache generation stays the same and the user cache is not flushed if there are no changes");
const cacheIntervalBeforeTest = assert.commandWorked(
db1.adminCommand({getParameter: 1, userCacheInvalidationIntervalSecs: 1}),
).userCacheInvalidationIntervalSecs;
// Set the invalidation interval for 5 seconds
assert.commandWorked(db1.adminCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 5}));
// Get current cache generation
const cfg1 = st.configRS.getPrimary().getDB("admin");
assert(cfg1.auth("root", "pwd"));
const genBeforeChange = assert.commandWorked(cfg1.runCommand({_getUserCacheGeneration: 1}));
// Update user role so cacheGeneration gets updated and grab current time
let currentTime = Date.now();
db1.getSiblingDB("admin").revokePrivilegesFromRole("myRole", [
{resource: {db: "test", collection: ""}, actions: ["dbStats"]},
]);
// Look for cache generation change that happened after currentTime
let genAfterChange;
assert.soon(() => {
genAfterChange = assert.commandWorked(cfg1.runCommand({_getUserCacheGeneration: 1}));
return genBeforeChange.cacheGeneration != genAfterChange.cacheGeneration;
});
// Set userCacheInvalidationIntervalSecs to 1 second and check current cache generation does not
// change after no user changes
currentTime = Date.now();
assert.commandWorked(db1.adminCommand({setParameter: 1, userCacheInvalidationIntervalSecs: 1}));
// Sleep 3 seconds to give enough time for the userCacheInvalidator job to run
sleep(3000);
const genAfterNoChange = assert.commandWorked(cfg1.runCommand({_getUserCacheGeneration: 1}));
assert.eq(
genAfterChange.cacheGeneration,
genAfterNoChange.cacheGeneration,
"User cache generation changed after no user change.",
);
cfg1.logout();
// Set userCacheInvalidationInterval back to value before the test
assert.commandWorked(
db1.adminCommand({setParameter: 1, userCacheInvalidationIntervalSecs: cacheIntervalBeforeTest}),
);
})();
(function testModifyingUser() {
jsTestLog("Testing propagation modifications to a user, rather than to a role");
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
hasAuthzError(db2.foo.update({}, {$inc: {a: 1}}));
hasAuthzError(db3.foo.update({}, {$inc: {a: 1}}));
db1.getSiblingDB("test").grantRolesToUser("spencer", ["readWrite"]);
// s0/db1 should update its cache instantly
assert.commandWorked(db1.foo.update({}, {$inc: {a: 1}}));
// s1/db2 should update its cache in 10 seconds.
sleep(10000);
assert.soon(
function () {
return !(db2.foo.update({}, {$inc: {a: 1}}) instanceof WriteCommandError);
},
"Mongos did not update its user cache after 10 seconds",
5 * 1000 /* Additional 5 seconds of buffer in case of slow update */,
);
// We manually invalidate the cache on s1/db3.
db3.adminCommand("invalidateUserCache");
assert.commandWorked(db3.foo.update({}, {$inc: {a: 1}}));
})();
(function testConcurrentUserModification() {
jsTestLog("Testing having 2 mongoses modify the same user at the same time"); // SERVER-13850
assert.commandWorked(db1.foo.update({}, {$inc: {a: 1}}));
assert.commandWorked(db3.foo.update({}, {$inc: {a: 1}}));
db1.getSiblingDB("test").revokeRolesFromUser("spencer", ["readWrite"]);
// At this point db3 still thinks "spencer" has readWrite. Use it to add a different role
// and make sure it doesn't add back readWrite
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
assert.commandWorked(db3.foo.update({}, {$inc: {a: 1}}));
db3.getSiblingDB("test").grantRolesToUser("spencer", ["dbAdmin"]);
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
// modifying "spencer" should force db3 to update its cache entry for "spencer"
hasAuthzError(db3.foo.update({}, {$inc: {a: 1}}));
// Make sure nothing changes from invalidating the cache
db1.adminCommand("invalidateUserCache");
db3.adminCommand("invalidateUserCache");
hasAuthzError(db1.foo.update({}, {$inc: {a: 1}}));
hasAuthzError(db3.foo.update({}, {$inc: {a: 1}}));
})();
(function testDroppingUser() {
jsTestLog("Testing propagation of dropping users");
assert.commandWorked(db1.foo.runCommand("collStats"));
assert.commandWorked(db2.foo.runCommand("collStats"));
assert.commandWorked(db3.foo.runCommand("collStats"));
db1.dropUser("spencer");
// s0/db1 should update its cache instantly
assert.commandFailedWithCode(db1.foo.runCommand("collStats"), authzErrorCode);
// s1/db2 should update its cache in 10 seconds.
sleep(10000);
assert.soon(
function () {
return db2.foo.runCommand("collStats").code == authzErrorCode;
},
"Mongos did not update its user cache after 10 seconds",
5 * 1000 /* Additional 5 seconds of buffer in case of slow update */,
);
// We manually invalidate the cache on s2/db3.
db3.adminCommand("invalidateUserCache");
assert.commandFailedWithCode(db3.foo.runCommand("collStats"), authzErrorCode);
})();
(function testStaticCacheGeneration() {
jsTestLog("Testing that cache generations stay static across config server authentication");
const cfg1 = st.configRS.getPrimary().getDB("admin");
assert(cfg1.auth("root", "pwd"));
// Create a previously unauthenticated user which is not in the authorization cached
assert.commandWorked(cfg1.runCommand({createUser: "previouslyUncached", pwd: "pwd", roles: []}));
const oldRes = assert.commandWorked(cfg1.runCommand({_getUserCacheGeneration: 1}));
// Authenticate as the uncached user
cfg1.logout();
assert(cfg1.auth("previouslyUncached", "pwd"));
cfg1.logout();
assert(cfg1.auth("root", "pwd"));
const newRes = assert.commandWorked(cfg1.runCommand({_getUserCacheGeneration: 1}));
assert.eq(
oldRes.cacheGeneration,
newRes.cacheGeneration,
"User cache generation supriously incremented on config servers",
);
// Put connection to config server back into default state before shutdown
cfg1.logout();
})();
st.stop();
print("SUCCESS Completed mongos_cache_invalidation.js");