Files
mongo/jstests/sharding/aggregation_currentop.js
2017-10-30 23:00:02 -04:00

617 lines
24 KiB
JavaScript

/**
* Tests that the $currentOp aggregation stage behaves as expected. Specifically:
* - It must be the first stage in the pipeline.
* - It can only be run on admin, and the "aggregate" field must be 1.
* - Only active connections are shown unless {idleConnections: true} is specified.
* - A user without the inprog privilege can see their own ops, but no-one else's.
* - A user with the inprog privilege can see all ops.
* - Non-local readConcerns are rejected.
* - Collation rules are respected.
*
* Also verifies that the aggregation-backed currentOp command obeys the same rules, where
* applicable.
*
* This test requires replica set configuration and user credentials to persist across a restart.
* @tags: [requires_persistence]
*/
(function() {
"use strict";
const key = "jstests/libs/key1";
// Create a new sharded cluster for testing. We set the internalQueryExecYieldIterations
// parameter so that plan execution yields on every iteration. For some tests, we will
// temporarily set yields to hang the mongod so we can capture particular operations in the
// currentOp output.
const st = new ShardingTest({
name: jsTestName(),
keyFile: key,
shards: 3,
rs: {
nodes: [
{rsConfig: {priority: 1}},
{rsConfig: {priority: 0}},
{rsConfig: {arbiterOnly: true}}
],
setParameter: {internalQueryExecYieldIterations: 1}
}
});
// Assign various elements of the cluster. We will use shard rs0 to test replica-set level
// $currentOp behaviour.
let shardConn = st.rs0.getPrimary();
const mongosConn = st.s;
const shardRS = st.rs0;
const clusterTestDB = mongosConn.getDB(jsTestName());
const clusterAdminDB = mongosConn.getDB("admin");
let shardAdminDB = shardConn.getDB("admin");
function createUsers(conn) {
let adminDB = conn.getDB("admin");
// Create an admin user, one user with the inprog privilege, and one without.
assert.commandWorked(
adminDB.runCommand({createUser: "admin", pwd: "pwd", roles: ["root"]}));
assert(adminDB.auth("admin", "pwd"));
assert.commandWorked(adminDB.runCommand({
createRole: "role_inprog",
roles: [],
privileges: [{resource: {cluster: true}, actions: ["inprog"]}]
}));
assert.commandWorked(
adminDB.runCommand({createUser: "user_inprog", pwd: "pwd", roles: ["role_inprog"]}));
assert.commandWorked(adminDB.runCommand(
{createUser: "user_no_inprog", pwd: "pwd", roles: ["readAnyDatabase"]}));
}
// Create necessary users at both cluster and shard-local level.
createUsers(shardConn);
createUsers(mongosConn);
// Create a test database and some dummy data on rs0.
assert(clusterAdminDB.auth("admin", "pwd"));
for (let i = 0; i < 5; i++) {
assert.writeOK(clusterTestDB.test.insert({_id: i, a: i}));
}
st.ensurePrimaryShard(clusterTestDB.getName(), shardRS.name);
// Restarts a replica set with additional parameters, and optionally re-authenticates.
function restartReplSet(replSet, newOpts, user, pwd) {
const numNodes = replSet.nodeList().length;
for (let n = 0; n < numNodes; n++) {
replSet.restart(n, newOpts);
}
shardConn = replSet.getPrimary();
replSet.awaitSecondaryNodes();
shardAdminDB = shardConn.getDB("admin");
if (user && pwd) {
shardAdminDB.auth(user, pwd);
}
}
// Functions to support running an operation in a parallel shell for testing allUsers behaviour.
function runInParallelShell({conn, testfunc, username, password}) {
TestData.aggCurOpTest = testfunc;
TestData.aggCurOpUser = username;
TestData.aggCurOpPwd = password;
assert.commandWorked(conn.getDB("admin").runCommand(
{configureFailPoint: "setYieldAllLocksHang", mode: "alwaysOn"}));
testfunc = function() {
db.getSiblingDB("admin").auth(TestData.aggCurOpUser, TestData.aggCurOpPwd);
TestData.aggCurOpTest();
db.getSiblingDB("admin").logout();
};
return startParallelShell(testfunc, conn.port);
}
function assertCurrentOpHasSingleMatchingEntry({conn, currentOpAggFilter, curOpOpts}) {
curOpOpts = (curOpOpts || {allUsers: true});
const connAdminDB = conn.getDB("admin");
let curOpResult;
assert.soon(
function() {
curOpResult =
connAdminDB.aggregate([{$currentOp: curOpOpts}, {$match: currentOpAggFilter}])
.toArray();
return curOpResult.length === 1;
},
function() {
return "Failed to find operation " + tojson(currentOpAggFilter) +
" in $currentOp output: " + tojson(curOpResult);
});
return curOpResult[0];
}
function waitForParallelShell(conn, awaitShell) {
assert.commandWorked(conn.getDB("admin").runCommand(
{configureFailPoint: "setYieldAllLocksHang", mode: "off"}));
awaitShell();
}
function getCollectionNameFromFullNamespace(ns) {
return ns.split(/\.(.+)/)[1];
}
// Generic function for running getMore on a $currentOp aggregation cursor and returning the
// command response.
function getMoreTest({conn, showAllUsers, getMoreBatchSize}) {
// Ensure that there are some other connections present so that the result set is larger
// than 1 $currentOp entry.
const otherConns = [new Mongo(conn.host), new Mongo(conn.host)];
// Log the other connections in as user_no_inprog so that they will show up for user_inprog
// with {allUsers: true} and user_no_inprog with {allUsers: false}.
for (let otherConn of otherConns) {
assert(otherConn.getDB("admin").auth("user_no_inprog", "pwd"));
}
const connAdminDB = conn.getDB("admin");
const aggCmdRes = assert.commandWorked(connAdminDB.runCommand({
aggregate: 1,
pipeline: [{$currentOp: {allUsers: showAllUsers, idleConnections: true}}],
cursor: {batchSize: 0}
}));
assert.neq(aggCmdRes.cursor.id, 0);
return connAdminDB.runCommand({
getMore: aggCmdRes.cursor.id,
collection: getCollectionNameFromFullNamespace(aggCmdRes.cursor.ns),
batchSize: (getMoreBatchSize || 100)
});
}
// Runs a suite of tests for behaviour common to both the replica set and cluster levels.
function runCommonTests(conn) {
const testDB = conn.getDB(jsTestName());
const adminDB = conn.getDB("admin");
const isMongos = (conn == mongosConn);
// Test that an unauthenticated connection cannot run $currentOp even with {allUsers:
// false}.
assert(adminDB.logout());
assert.commandFailedWithCode(
adminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {allUsers: false}}], cursor: {}}),
ErrorCodes.Unauthorized);
// Test that an unauthenticated connection cannot run the currentOp command even with
// {$ownOps: true}.
assert.commandFailedWithCode(adminDB.currentOp({$ownOps: true}), ErrorCodes.Unauthorized);
//
// Authenticate as user_no_inprog.
//
assert(adminDB.logout());
assert(adminDB.auth("user_no_inprog", "pwd"));
// Test that $currentOp fails with {allUsers: true} for a user without the "inprog"
// privilege.
assert.commandFailedWithCode(
adminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {allUsers: true}}], cursor: {}}),
ErrorCodes.Unauthorized);
// Test that the currentOp command fails with {ownOps: false} for a user without the
// "inprog" privilege.
assert.commandFailedWithCode(adminDB.currentOp({$ownOps: false}), ErrorCodes.Unauthorized);
// Test that {aggregate: 1} fails when the first stage in the pipeline is not $currentOp.
assert.commandFailedWithCode(
adminDB.runCommand({aggregate: 1, pipeline: [{$match: {}}], cursor: {}}),
ErrorCodes.InvalidNamespace);
//
// Authenticate as user_inprog.
//
assert(adminDB.logout());
assert(adminDB.auth("user_inprog", "pwd"));
// Test that $currentOp fails when it is not the first stage in the pipeline. We use two
// $currentOp stages since any other stage in the initial position will trip the {aggregate:
// 1} namespace check.
assert.commandFailedWithCode(
adminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {}}, {$currentOp: {}}], cursor: {}}),
40602);
// Test that $currentOp fails when run on admin without {aggregate: 1}.
assert.commandFailedWithCode(
adminDB.runCommand({aggregate: "collname", pipeline: [{$currentOp: {}}], cursor: {}}),
ErrorCodes.InvalidNamespace);
// Test that $currentOp fails when run as {aggregate: 1} on a database other than admin.
assert.commandFailedWithCode(
testDB.runCommand({aggregate: 1, pipeline: [{$currentOp: {}}], cursor: {}}),
ErrorCodes.InvalidNamespace);
// Test that the currentOp command fails when run directly on a database other than admin.
assert.commandFailedWithCode(testDB.runCommand({currentOp: 1}), ErrorCodes.Unauthorized);
// Test that the currentOp command helper succeeds when run on a database other than admin.
// This is because the currentOp shell helper redirects the command to the admin database.
assert.commandWorked(testDB.currentOp());
// Test that $currentOp and the currentOp command accept all numeric types.
const ones = [1, 1.0, NumberInt(1), NumberLong(1), NumberDecimal(1)];
for (let one of ones) {
assert.commandWorked(
adminDB.runCommand({aggregate: one, pipeline: [{$currentOp: {}}], cursor: {}}));
assert.commandWorked(adminDB.runCommand({currentOp: one, $ownOps: true}));
}
// Test that $currentOp with {allUsers: true} succeeds for a user with the "inprog"
// privilege.
assert.commandWorked(adminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {allUsers: true}}], cursor: {}}));
// Test that the currentOp command with {$ownOps: false} succeeds for a user with the
// "inprog" privilege.
assert.commandWorked(adminDB.currentOp({$ownOps: false}));
// Test that $currentOp succeeds if local readConcern is specified.
assert.commandWorked(adminDB.runCommand({
aggregate: 1,
pipeline: [{$currentOp: {}}],
readConcern: {level: "local"},
cursor: {}
}));
// Test that $currentOp fails if a non-local readConcern is specified.
assert.commandFailedWithCode(adminDB.runCommand({
aggregate: 1,
pipeline: [{$currentOp: {}}],
readConcern: {level: "linearizable"},
cursor: {}
}),
ErrorCodes.InvalidOptions);
// Test that {idleConnections: false} returns only active connections.
const idleConn = new Mongo(conn.host);
assert.eq(adminDB
.aggregate([
{$currentOp: {allUsers: true, idleConnections: false}},
{$match: {active: false}}
])
.itcount(),
0);
// Test that the currentOp command with {$all: false} returns only active connections.
assert.eq(adminDB.currentOp({$ownOps: false, $all: false, active: false}).inprog.length, 0);
// Test that {idleConnections: true} returns inactive connections.
assert.gte(adminDB
.aggregate([
{$currentOp: {allUsers: true, idleConnections: true}},
{$match: {active: false}}
])
.itcount(),
1);
// Test that the currentOp command with {$all: true} returns inactive connections.
assert.gte(adminDB.currentOp({$ownOps: false, $all: true, active: false}).inprog.length, 1);
// Test that collation rules apply to matches on $currentOp output.
const matchField = (isMongos ? "originatingCommand.comment" : "command.comment");
const numExpectedMatches = (isMongos ? 3 : 1);
assert.eq(
adminDB
.aggregate(
[{$currentOp: {}}, {$match: {[matchField]: "AGG_currént_op_COLLATION"}}],
{
collation: {locale: "en_US", strength: 1}, // Case and diacritic insensitive.
comment: "agg_current_op_collation"
})
.itcount(),
numExpectedMatches);
// Test that $currentOp output can be processed by $facet subpipelines.
assert.eq(adminDB
.aggregate(
[
{$currentOp: {}},
{
$facet: {
testFacet: [
{$match: {[matchField]: "agg_current_op_facets"}},
{$count: "count"}
]
}
},
{$unwind: "$testFacet"},
{$replaceRoot: {newRoot: "$testFacet"}}
],
{comment: "agg_current_op_facets"})
.next()
.count,
numExpectedMatches);
// Test that $currentOp is explainable.
const explainPlan = assert.commandWorked(adminDB.runCommand({
aggregate: 1,
pipeline:
[{$currentOp: {idleConnections: true, allUsers: false}}, {$match: {desc: "test"}}],
explain: true
}));
const expectedStages = [
{$currentOp: {idleConnections: true, allUsers: false, truncateOps: false}},
{$match: {desc: {$eq: "test"}}}
];
if (isMongos) {
assert.eq(explainPlan.splitPipeline.shardsPart, expectedStages);
for (let i = 0; i < 3; i++) {
let shardName = st["rs" + i].name;
assert.eq(explainPlan.shards[shardName].stages, expectedStages);
}
} else {
assert.eq(explainPlan.stages, expectedStages);
}
// Test that a user with the inprog privilege can run getMore on a $currentOp aggregation
// cursor which they created with {allUsers: true}.
let getMoreCmdRes = assert.commandWorked(
getMoreTest({conn: conn, showAllUsers: true, getMoreBatchSize: 1}));
// Test that a user without the inprog privilege cannot run getMore on a $currentOp
// aggregation cursor created by a user with {allUsers: true}.
assert(adminDB.logout());
assert(adminDB.auth("user_no_inprog", "pwd"));
assert.neq(getMoreCmdRes.cursor.id, 0);
assert.commandFailedWithCode(adminDB.runCommand({
getMore: getMoreCmdRes.cursor.id,
collection: getCollectionNameFromFullNamespace(getMoreCmdRes.cursor.ns),
batchSize: 100
}),
ErrorCodes.Unauthorized);
}
runCommonTests(shardConn);
runCommonTests(mongosConn);
//
// mongoS specific tests.
//
// Test that a user without the inprog privilege cannot run cluster $currentOp via mongoS even
// if allUsers is false.
assert(clusterAdminDB.logout());
assert(clusterAdminDB.auth("user_no_inprog", "pwd"));
assert.commandFailedWithCode(
clusterAdminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {allUsers: false}}], cursor: {}}),
ErrorCodes.Unauthorized);
// Test that a user without the inprog privilege cannot run the currentOp command via mongoS
// even if $ownOps is true.
assert.commandFailedWithCode(clusterAdminDB.currentOp({$ownOps: true}),
ErrorCodes.Unauthorized);
// Test that a $currentOp pipeline returns results from all shards, and includes both the shard
// and host names.
assert(clusterAdminDB.logout());
assert(clusterAdminDB.auth("user_inprog", "pwd"));
assert.eq(clusterAdminDB
.aggregate([
{$currentOp: {allUsers: true, idleConnections: true}},
{$group: {_id: {shard: "$shard", host: "$host"}}},
{$sort: {_id: 1}}
])
.toArray(),
[
{_id: {shard: "aggregation_currentop-rs0", host: st.rs0.getPrimary().host}},
{_id: {shard: "aggregation_currentop-rs1", host: st.rs1.getPrimary().host}},
{_id: {shard: "aggregation_currentop-rs2", host: st.rs2.getPrimary().host}}
]);
//
// ReplSet specific tests.
//
// Test that a user with the inprog privilege can see another user's operations with {allUsers:
// true} when run on a mongoD.
assert(shardAdminDB.logout());
assert(shardAdminDB.auth("user_inprog", "pwd"));
let awaitShell = runInParallelShell({
testfunc: function() {
assert.eq(db.getSiblingDB(jsTestName())
.test.find({})
.comment("agg_current_op_allusers_test")
.itcount(),
5);
},
conn: shardConn,
username: "admin",
password: "pwd"
});
assertCurrentOpHasSingleMatchingEntry(
{conn: shardConn, currentOpAggFilter: {"command.comment": "agg_current_op_allusers_test"}});
// Test that the currentOp command can see another user's operations with {$ownOps: false}.
assert.eq(
shardAdminDB.currentOp({$ownOps: false, "command.comment": "agg_current_op_allusers_test"})
.inprog.length,
1);
// Allow the op to complete.
waitForParallelShell(shardConn, awaitShell);
// Test that $currentOp succeeds with {allUsers: false} for a user without the "inprog"
// privilege when run on a mongoD.
assert(shardAdminDB.logout());
assert(shardAdminDB.auth("user_no_inprog", "pwd"));
assert.commandWorked(shardAdminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {allUsers: false}}], cursor: {}}));
// Test that the currentOp command succeeds with {$ownOps: true} for a user without the "inprog"
// privilege when run on a mongoD.
assert.commandWorked(shardAdminDB.currentOp({$ownOps: true}));
// Test that a user without the inprog privilege cannot see another user's operations.
// Temporarily log in as 'user_inprog' to validate that the op is present in $currentOp output.
assert(shardAdminDB.logout());
assert(shardAdminDB.auth("user_inprog", "pwd"));
awaitShell = runInParallelShell({
testfunc: function() {
assert.eq(db.getSiblingDB(jsTestName())
.test.find({})
.comment("agg_current_op_allusers_test")
.itcount(),
5);
},
conn: shardConn,
username: "admin",
password: "pwd"
});
assertCurrentOpHasSingleMatchingEntry({
currentOpAggFilter: {"command.comment": "agg_current_op_allusers_test"},
curOpOpts: {allUsers: true},
conn: shardConn
});
// Log back in as 'user_no_inprog' and validate that the user cannot see the op.
assert(shardAdminDB.logout());
assert(shardAdminDB.auth("user_no_inprog", "pwd"));
assert.eq(shardAdminDB
.aggregate([
{$currentOp: {allUsers: false}},
{$match: {"command.comment": "agg_current_op_allusers_test"}}
])
.itcount(),
0);
// Test that a user without the inprog privilege cannot see another user's operations via the
// currentOp command.
assert.eq(
shardAdminDB.currentOp({$ownOps: true, "command.comment": "agg_current_op_allusers_test"})
.inprog.length,
0);
waitForParallelShell(shardConn, awaitShell);
// Test that a user without the inprog privilege can run getMore on a $currentOp cursor which
// they created with {allUsers: false}.
assert.commandWorked(getMoreTest({conn: shardConn, showAllUsers: false}));
// Test that the allUsers parameter is ignored when authentication is disabled.
restartReplSet(shardRS, {shardsvr: null, keyFile: null});
// Ensure that there is at least one other connection present.
const otherConn = new Mongo(shardConn.host);
// Verify that $currentOp displays all operations when auth is disabled regardless of the
// allUsers parameter, by confirming that we can see non-client system operations when
// {allUsers: false} is specified.
assert.gte(shardAdminDB
.aggregate([
{$currentOp: {allUsers: false, idleConnections: true}},
{$match: {connectionId: {$exists: false}}}
])
.itcount(),
1);
// Verify that the currentOp command displays all operations when auth is disabled regardless of
// the $ownOps parameter, by confirming that we can see non-client system operations when
// {$ownOps: true} is specified.
assert.gte(shardAdminDB.currentOp({$ownOps: true, $all: true, connectionId: {$exists: false}})
.inprog.length,
1);
// Test that a user can run getMore on a $currentOp cursor when authentication is disabled.
assert.commandWorked(getMoreTest({conn: shardConn, showAllUsers: true}));
// Test that the host field is present and the shard field is absent when run on mongoD.
assert.eq(shardAdminDB
.aggregate([
{$currentOp: {allUsers: true, idleConnections: true}},
{$group: {_id: {shard: "$shard", host: "$host"}}}
])
.toArray(),
[
{_id: {host: shardConn.host}},
]);
// Test that attempting to 'spoof' a sharded request on non-shardsvr mongoD fails.
assert.commandFailedWithCode(
shardAdminDB.runCommand(
{aggregate: 1, pipeline: [{$currentOp: {}}], fromMongos: true, cursor: {}}),
40465);
// Test that an operation which is at the BSON user size limit does not throw an error when the
// currentOp metadata is added to the output document.
const bsonUserSizeLimit = assert.commandWorked(shardAdminDB.isMaster()).maxBsonObjectSize;
let aggPipeline = [
{$currentOp: {}},
{
$match: {
$or: [
{
"command.comment": "agg_current_op_bson_limit_test",
"command.$truncated": {$exists: false}
},
{padding: ""}
]
}
}
];
aggPipeline[1].$match.$or[1].padding =
"a".repeat(bsonUserSizeLimit - Object.bsonsize(aggPipeline));
assert.eq(Object.bsonsize(aggPipeline), bsonUserSizeLimit);
assert.eq(
shardAdminDB.aggregate(aggPipeline, {comment: "agg_current_op_bson_limit_test"}).itcount(),
1);
// Test that $currentOp can run while the mongoD is write-locked.
awaitShell = startParallelShell(function() {
assert.commandFailedWithCode(db.adminCommand({sleep: 1, lock: "w", secs: 300}),
ErrorCodes.Interrupted);
}, shardConn.port);
const op = assertCurrentOpHasSingleMatchingEntry(
{conn: shardConn, currentOpAggFilter: {"command.sleep": 1, active: true}});
assert.commandWorked(shardAdminDB.killOp(op.opid));
awaitShell();
})();