mongo/jstests/libs/kill_sessions.js

800 lines
27 KiB
JavaScript

import {Thread} from "jstests/libs/parallelTester.js";
/**
* Implements a kill session test helper
*/
export var _kill_sessions_api_module = (function () {
let KillSessionsTestHelper = {};
function isdbgrid(client) {
let result = assert.commandWorked(client.getDB("admin").runCommand({ismaster: 1}));
return result.msg === "isdbgrid";
}
function Fixture(clientToExecuteVia, clientToKillVia, clientsToVerifyVia) {
this._clientToExecuteVia = clientToExecuteVia;
this._clientToExecuteViaIsdbgrid = isdbgrid(clientToExecuteVia);
this._clientToKillVia = clientToKillVia;
this._clientToKillViaIsdbgrid = isdbgrid(clientToKillVia);
this._clientsToVerifyVia = clientsToVerifyVia;
this._hangingOpId = 10000000;
}
Fixture.prototype.loginForExecute = function (cred) {
this._clientToExecuteVia.getDB("admin").auth(cred, "password");
this._clientToExecuteViaCredentials = cred;
};
Fixture.prototype.loginForKill = function (cred) {
this._clientToKillVia.getDB("admin").auth(cred, "password");
this._clientToKillViaCredentials = cred;
};
Fixture.prototype.logout = function (db) {
this._clientToExecuteVia.logout("admin");
this._clientToKillVia.logout("admin");
};
Fixture.prototype.kill = function (db, command) {
let result = this._clientToKillVia.getDB(db).runCommand(command);
if (!result.ok) {
jsTest.log.info({result});
}
assert(result.ok);
};
Fixture.prototype.assertKillFailed = function (db, command) {
let result = this._clientToKillVia.getDB(db).runCommand(command);
if (result.ok) {
jsTest.log.info({result, command});
}
assert(!result.ok);
};
Fixture.prototype.visit = function (cb) {
for (let i = 0; i < this._clientsToVerifyVia.length; ++i) {
cb(this._clientsToVerifyVia[i], i);
}
};
function HangingOpHandle(thread, lsid) {
this._thread = thread;
this._lsid = lsid;
}
HangingOpHandle.prototype.join = function () {
return this._thread.join();
};
HangingOpHandle.prototype.getLsid = function () {
return this._lsid;
};
/**
* We start hanging ops by running the test command sleep with a special number of secs that's
* meant to be universal per test run. In mongos, we multicast it out (to guarantee it's on all
* of the random accessory nodes).
*/
Fixture.prototype.startHangingOp = function () {
let id = this._hangingOpId++;
// When creating a hanging op, we have to use a background thread (because the command will
// hang).
let thread = new Thread(
function (connstring, credentials, tag, isdbgrid) {
let client = new Mongo(connstring);
if (credentials) {
client.getDB("admin").auth(credentials, "password");
}
let session = client.startSession();
let db = session.getDatabase("admin");
try {
let cmd = {
sleep: 1,
lock: "none",
secs: tag,
};
if (isdbgrid) {
db.runCommand({
multicast: cmd,
});
} else {
db.runCommand(cmd);
}
} catch (e) {}
session.endSession();
client.close();
},
this._clientToExecuteVia.host,
this._clientToExecuteViaCredentials,
id,
this._clientToExecuteViaIsdbgrid,
);
thread.start();
let lsid;
// We verify that our hanging op is up by looking for it in current op on the required
// hosts. We identify particular ops by secs sleeping.
this.visit(function (client) {
let admin = client.getDB("admin");
admin.getMongo().setSecondaryOk();
assert.soon(
function () {
let inProgressOps = admin.aggregate([{$currentOp: {"allUsers": true}}]);
while (inProgressOps.hasNext()) {
let op = inProgressOps.next();
if (op.command && op.command.sleep && op.command.secs == id && op.lsid) {
lsid = op.lsid;
return true;
}
}
return false;
},
"never started sleep with 'secs' " + id,
30000,
1,
);
});
return new HangingOpHandle(thread, lsid);
};
/**
* passes the lsid of each running op into our callback. Makes it easy to ensure that a session
* is well and truly dead
*/
Fixture.prototype.assertNoSessionsInCurrentOp = function (cb) {
this.visit(function (client) {
let inprog = client.getDB("admin").currentOp().inprog;
inprog.forEach(function (op) {
assert(op.killPending || !op.lsid);
});
});
};
Fixture.prototype.assertSessionsInCurrentOp = function (checkExist, checkNotExist) {
this.visit(function (client) {
let needToFind = checkExist.map(function (handle) {
return handle.getLsid();
});
let inprog = client.getDB("admin").currentOp().inprog;
inprog.forEach(function (op) {
if (op.lsid && !op.killPending) {
checkNotExist.forEach(function (handle) {
assert.neq(bsonWoCompare(handle.getLsid(), op.lsid), 0);
});
for (let i = 0; i < needToFind.length; ++i) {
if (bsonWoCompare(needToFind[i], op.lsid) == 0) {
needToFind.splice(i, 1);
break;
}
}
}
});
assert.eq(needToFind.length, 0);
});
};
/**
* Asserts that there are no sessions in any live cursors.
*/
Fixture.prototype.assertNoSessionsInCursors = function () {
this.visit(function (client) {
let db = client.getDB("admin");
db.setSecondaryOk();
assert.soon(() => {
let cursors = db.aggregate([{"$currentOp": {"idleCursors": true, "allUsers": true}}]).toArray();
return cursors.every((cursor) => !cursor.lsid);
});
});
};
/**
* Asserts that one subset of sessions is alive in active cursors and that another set is not.
*/
Fixture.prototype.assertSessionsInCursors = function (checkExist, checkNotExist) {
this.visit(function (client) {
let needToFind = checkExist.map(function (handle) {
return {
lsid: handle.getLsid(),
};
});
let db = client.getDB("admin");
db.setSecondaryOk();
let cursors = db
.aggregate([{"$currentOp": {"idleCursors": true, "allUsers": true}}, {"$match": {type: "idleCursor"}}])
.toArray();
cursors.forEach(function (cursor) {
if (cursor.lsid) {
checkNotExist.forEach(function (handle) {
assert.neq(bsonWoCompare({x: handle.getLsid().id}, {x: cursor.lsid.id}), 0);
});
for (let i = 0; i < needToFind.length; ++i) {
if (bsonWoCompare({x: needToFind[i].lsid.id}, {x: cursor.lsid.id}) == 0) {
needToFind.splice(i, 1);
break;
}
}
}
});
assert.eq(needToFind.length, 0, cursors);
});
};
Fixture.prototype.assertCursorKillLogMessages = function (cursorHandles) {
cursorHandles.forEach((cursorHandle) => cursorHandle.assertCursorKillLogMessages(this._clientsToVerifyVia));
};
function CursorHandle(session, cursors) {
this._session = session;
this._cursors = cursors;
}
CursorHandle.prototype.getLsid = function () {
return this._session._serverSession.handle.getId();
};
CursorHandle.prototype.join = function () {
this._session.endSession();
};
CursorHandle.prototype.assertCursorKillLogMessages = function (hostsToCheck) {
for (let hostToCheck of hostsToCheck) {
if (hostToCheck.host in this._cursors) {
assert(
checkLog.checkContainsOnceJsonStringMatch(
hostToCheck,
20528,
"cursorId",
this._cursors[hostToCheck.host].exactValueString,
),
"cursor kill was not logged by " + hostToCheck.host,
);
}
}
};
/**
* We start cursors on all nodes in a very artificial way. In particular, we run currentOp
* (because it's a data source that's always there) and use an agg pipeline to get enough
* records that we can't return it in one batch. In sharding, we use multicast to force open
* cursors on all nodes at once.
*/
Fixture.prototype.startCursor = function () {
let session = this._clientToExecuteVia.startSession();
let db = session.getDatabase("admin");
let cmd = {
aggregate: 1,
pipeline: [
{"$currentOp": {"allUsers": false}},
{"$addFields": {magic: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}},
{"$project": {magic: 1}},
{"$unwind": "$magic"},
],
cursor: {batchSize: 1},
"$readPreference": {
mode: "primaryPreferred",
},
readConcern: {},
writeConcern: {w: 1},
};
let cursors = {};
let result;
if (this._clientToExecuteViaIsdbgrid) {
result = db.runCommand({multicast: cmd});
for (let host of Object.keys(result.hosts)) {
cursors[host] = result.hosts[host].data.cursor.id;
}
} else {
result = db.runCommand(cmd);
cursors[db.getMongo().host] = result.cursor.id;
}
if (!result.ok) {
jsTest.log.info({result});
}
assert(result.ok);
return new CursorHandle(session, cursors);
};
/**
* A suite of tests verifying that various forms of no argument kill work. Outside of auth
*/
function makeNoAuthNoArgKill(cmd) {
let obj = {};
obj[cmd] = [];
return [
// Verify that we can invoke cmd
function (fixture) {
fixture.kill("admin", obj);
},
// Verify that we can start a session and kill it with cmd
function (fixture) {
let handle = fixture.startHangingOp();
fixture.kill("admin", obj);
fixture.assertNoSessionsInCurrentOp();
handle.join();
},
// Verify that we can kill two sessions
function (fixture) {
let handle1 = fixture.startHangingOp();
let handle2 = fixture.startHangingOp();
fixture.kill("admin", obj);
fixture.assertNoSessionsInCurrentOp();
handle1.join();
handle2.join();
},
// Verify that we can start a session with a cursor and kill it with cmd
function (fixture) {
let handle = fixture.startCursor();
fixture.assertSessionsInCursors([handle], []);
fixture.kill("admin", obj);
fixture.assertNoSessionsInCursors();
fixture.assertCursorKillLogMessages([handle]);
handle.join();
},
// Verify that we can kill two sessions with cursors
function (fixture) {
let handle1 = fixture.startCursor();
let handle2 = fixture.startCursor();
fixture.assertSessionsInCursors([handle1, handle2], []);
fixture.kill("admin", obj);
fixture.assertNoSessionsInCursors();
fixture.assertCursorKillLogMessages([handle1, handle2]);
handle1.join();
handle2.join();
},
];
}
/**
* Verifies that various argument taking kills work. Outside of auth
*/
function makeNoAuthArgKill(cmd, genArg) {
return [
// Verify that we can kill two of three sessions, and that the other stays alive
function (fixture) {
let handle1 = fixture.startHangingOp();
let handle2 = fixture.startHangingOp();
let handle3 = fixture.startHangingOp();
{
var obj = {};
obj[cmd] = [handle1.getLsid(), handle2.getLsid()].map(genArg);
fixture.kill("admin", obj);
}
fixture.assertSessionsInCurrentOp([handle3], [handle1, handle2]);
handle1.join();
handle2.join();
{
var obj = {};
obj[cmd] = [handle3.getLsid()].map(genArg);
fixture.kill("admin", obj);
}
fixture.assertNoSessionsInCurrentOp();
handle3.join();
},
// Verify that we can kill two of three sessions, and that the other stays (with
// cursors)
function (fixture) {
let handle1 = fixture.startCursor();
let handle2 = fixture.startCursor();
let handle3 = fixture.startCursor();
fixture.assertSessionsInCursors([handle1, handle2, handle3], []);
{
var obj = {};
obj[cmd] = [handle1.getLsid(), handle2.getLsid()].map(genArg);
fixture.kill("admin", obj);
}
fixture.assertSessionsInCursors([handle3], [handle1, handle2]);
fixture.assertCursorKillLogMessages([handle1, handle2]);
handle1.join();
handle2.join();
{
var obj = {};
obj[cmd] = [handle3.getLsid()].map(genArg);
fixture.kill("admin", obj);
}
fixture.assertNoSessionsInCursors();
fixture.assertCursorKillLogMessages([handle3]);
handle3.join();
},
];
}
let noAuth = [
// Verify that we can kill two sessions with the kill all args from killAllSessionsByPattern
function (fixture) {
let handle1 = fixture.startHangingOp();
let handle2 = fixture.startHangingOp();
fixture.kill("admin", {killAllSessionsByPattern: [{}]});
fixture.assertNoSessionsInCurrentOp();
handle1.join();
handle2.join();
},
];
// Runs our noarg suite for all commands
["killSessions", "killAllSessions", "killAllSessionsByPattern"].forEach(function (cmd) {
noAuth = noAuth.concat(makeNoAuthNoArgKill(cmd));
});
[
[
// Verifies that we can killSessions by lsid
"killSessions",
function (x) {
if (!x.uid) {
return {
id: x.id,
uid: computeSHA256Block(""),
};
} else {
return x;
}
},
],
[
// Verifies that we can kill by pattern by lsid
"killAllSessionsByPattern",
function (x) {
if (!x.uid) {
return {
lsid: {
id: x.id,
uid: computeSHA256Block(""),
},
};
} else {
return {lsid: x};
}
},
],
].forEach(function (cmd) {
noAuth = noAuth.concat(makeNoAuthArgKill.apply({}, cmd));
});
KillSessionsTestHelper.runNoAuth = function (clientToExecuteVia, clientToKillVia, clientsToVerifyVia) {
let fixture = new Fixture(clientToExecuteVia, clientToKillVia, clientsToVerifyVia);
for (let i = 0; i < noAuth.length; ++i) {
noAuth[i](fixture);
}
};
// Run tests for a command that takes no args in auth
function makeAuthNoArgKill(cmd, execUserCred, killUserCred) {
let obj = {};
obj[cmd] = [];
return [
// Verify that we can invoke cmd
function (fixture) {
fixture.loginForExecute(execUserCred);
fixture.loginForKill(killUserCred);
fixture.kill("admin", obj);
},
// Verify that we can start a session and kill it with cmd
function (fixture) {
fixture.loginForExecute(execUserCred);
fixture.loginForKill(killUserCred);
let handle = fixture.startHangingOp();
fixture.kill("admin", obj);
fixture.assertNoSessionsInCurrentOp();
handle.join();
},
];
}
// Run tests for a command that takes args in auth.
//
// The genArg argument is a function which returns another function given a username. It's
// meant to adapt various lsid's from ops/cursors to various commands.
function makeAuthArgKill(cmd, execUserCred1, execUserCred2, killUserCred, genArg) {
return [
// Run 3 ops, 2 under 1 user, 1 under another. Then kill, making sure that the 3rd op
// stays up
function (fixture) {
fixture.loginForExecute(execUserCred1);
let handle1 = fixture.startHangingOp();
let handle2 = fixture.startHangingOp();
fixture.logout();
fixture.loginForExecute(execUserCred2);
let handle3 = fixture.startHangingOp();
fixture.loginForKill(killUserCred);
{
var obj = {};
obj[cmd] = [handle1.getLsid(), handle2.getLsid()].map(genArg(execUserCred1));
fixture.kill("admin", obj);
}
fixture.assertSessionsInCurrentOp([handle3], [handle1, handle2]);
handle1.join();
handle2.join();
{
var obj = {};
obj[cmd] = [handle3.getLsid()].map(genArg(execUserCred2));
fixture.kill("admin", obj);
}
fixture.assertNoSessionsInCurrentOp();
handle3.join();
},
// Repeat for cursors
function (fixture) {
fixture.loginForExecute(execUserCred1);
let handle1 = fixture.startCursor();
let handle2 = fixture.startCursor();
fixture.logout();
fixture.loginForExecute(execUserCred2);
let handle3 = fixture.startCursor();
fixture.assertSessionsInCursors([handle1, handle2, handle3], []);
fixture.loginForKill(killUserCred);
{
var obj = {};
obj[cmd] = [handle1.getLsid(), handle2.getLsid()].map(genArg(execUserCred1));
fixture.kill("admin", obj);
}
fixture.assertSessionsInCursors([handle3], [handle1, handle2]);
fixture.assertCursorKillLogMessages([handle1, handle2]);
handle1.join();
handle2.join();
{
var obj = {};
obj[cmd] = [handle3.getLsid()].map(genArg(execUserCred2));
fixture.kill("admin", obj);
}
fixture.assertNoSessionsInCursors();
fixture.assertCursorKillLogMessages([handle3]);
handle3.join();
},
];
}
let auth = [
// Verify that we can start a session and kill it with the universal pattern
function (fixture) {
fixture.loginForExecute("simple");
fixture.loginForKill("killAny");
let handle = fixture.startHangingOp();
fixture.kill("admin", {killAllSessionsByPattern: [{}]});
fixture.assertNoSessionsInCurrentOp();
handle.join();
},
// Verify that we can impersonate, with that permission
function (fixture) {
fixture.loginForExecute("simple");
fixture.loginForKill("impersonate");
let handle = fixture.startHangingOp();
fixture.kill("admin", {killAllSessionsByPattern: [{users: [], roles: []}]});
fixture.assertNoSessionsInCurrentOp();
handle.join();
},
];
// Tests for makeAuthNoArgKill
[
[
// We can kill our own sessions
"killSessions",
"simple",
"simple",
],
[
// We can kill all sessions
"killAllSessions",
"simple",
"killAny",
],
[
// We can kill all sessions by pattern
"killAllSessionsByPattern",
"simple",
"killAny",
],
].forEach(function (cmd) {
auth = auth.concat(makeAuthNoArgKill.apply({}, cmd));
});
// Tests for makeAuthArgKill
[
[
// We can kill our own sessions by id (spoofing our own id)
"killSessions",
"simple",
"simple",
"killAny",
function () {
return function (x) {
if (!x.uid) {
return {
id: x.id,
uid: computeSHA256Block("simple@admin"),
};
} else {
return x;
}
};
},
],
[
// We can kill our own sessions without spoofing
"killSessions",
"simple",
"simple",
"simple",
function () {
return function (x) {
return x;
};
},
],
[
// We can kill by pattern by id
"killAllSessionsByPattern",
"simple",
"simple",
"killAny",
function () {
return function (x) {
if (!x.uid) {
return {
lsid: {
id: x.id,
uid: computeSHA256Block("simple@admin"),
},
};
} else {
return {lsid: x};
}
};
},
],
[
// We can kill any by user
"killAllSessions",
"simple",
"simple2",
"killAny",
function (user) {
return function (x) {
return {db: "admin", user: user};
};
},
],
[
// We can kill any by pattern by user
"killAllSessionsByPattern",
"simple",
"simple2",
"killAny",
function (user) {
return function (x) {
return {uid: computeSHA256Block(user + "@admin")};
};
},
],
].forEach(function (cmd) {
auth = auth.concat(makeAuthArgKill.apply({}, cmd));
});
// Ensures that illegal things fail
function makeAuthArgKillFailure(cmd, execUserCred, killUserCred, genArg) {
return [
function (fixture) {
fixture.loginForExecute(execUserCred);
fixture.loginForKill(killUserCred);
let session = fixture._clientToExecuteVia.startSession();
let obj = {};
obj[cmd] = [session._serverSession.handle.getId()].map(genArg(execUserCred));
fixture.assertKillFailed("admin", obj);
session.endSession();
},
];
}
// Tests for makeAuthArgKillFailure
[
[
// We can't kill another users sessions
"killSessions",
"simple",
"simple2",
function (user) {
return function (x) {
return {
id: x.id,
uid: computeSHA256Block(user + "@admin"),
};
};
},
],
[
// We can't impersonate without impersonate
"killAllSessionsByPattern",
"simple",
"killAny",
function (user) {
return function (x) {
return {
users: {},
roles: {},
};
};
},
],
].forEach(function (cmd) {
auth = auth.concat(makeAuthArgKillFailure.apply({}, cmd));
});
KillSessionsTestHelper.runAuth = function (clientToExecuteVia, clientToKillVia, clientsToVerifyVia) {
let fixture = new Fixture(clientToExecuteVia, clientToKillVia, clientsToVerifyVia);
for (let i = 0; i < auth.length; ++i) {
fixture.logout();
auth[i](fixture);
}
};
KillSessionsTestHelper.initializeAuth = function (client) {
let admin = client.getDB("admin");
admin.createUser({user: "super", pwd: "password", roles: jsTest.adminUserRoles});
admin.auth("super", "password");
admin.createRole({
role: "killAnySession",
roles: [],
privileges: [{resource: {cluster: true}, actions: ["killAnySession"]}],
});
admin.createRole({
role: "forSimpleTest",
roles: [],
privileges: [{resource: {cluster: true}, actions: ["inprog"]}],
});
admin.createRole({
role: "forImpersonate",
roles: [],
privileges: [{resource: {cluster: true}, actions: ["impersonate"]}],
});
admin.createUser({user: "simple", pwd: "password", roles: ["forSimpleTest"]});
admin.createUser({user: "simple2", pwd: "password", roles: ["forSimpleTest"]});
admin.createUser({user: "killAny", pwd: "password", roles: ["killAnySession"]});
admin.createUser({user: "impersonate", pwd: "password", roles: ["forImpersonate", "killAnySession"]});
};
let module = {};
module.KillSessionsTestHelper = KillSessionsTestHelper;
return module;
})();
// Globals
export var KillSessionsTestHelper = _kill_sessions_api_module.KillSessionsTestHelper;