188 lines
5.7 KiB
JavaScript
188 lines
5.7 KiB
JavaScript
/**
|
|
* Overrides the runCommand method to set security token on connection, so that the requests send by
|
|
* client will pass the signed tenant information to server through the security token.
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
load("jstests/libs/override_methods/override_helpers.js"); // For 'OverrideHelpers'.
|
|
|
|
const kUserName = "userTenant1";
|
|
const kTenantId = ObjectId();
|
|
|
|
function createTenantUser(userName, tenantId) {
|
|
let prepareRootUser = (adminDb) => {
|
|
// Create a user which has root role so that it can be authenticated as a user with
|
|
// ActionType::useTenant in order to use $tenant.
|
|
let res = adminDb.runCommand(
|
|
{createUser: 'root', pwd: 'pwd', roles: ['root'], comment: 'skipOverride'});
|
|
if (res.ok === 1) {
|
|
assert.commandWorked(res);
|
|
print('Create a user "root" with the useTenant privilege successfully.');
|
|
} else {
|
|
// If 'username' already exists, then attempts to create a user with the same name
|
|
// will fail with error code 51003.
|
|
assert.commandFailedWithCode(res, 51003);
|
|
}
|
|
};
|
|
|
|
const adminDb = db.getSiblingDB('admin');
|
|
prepareRootUser(adminDb);
|
|
assert(adminDb.auth('root', 'pwd'));
|
|
|
|
// Create a user for tenant and then set the security token on the connection.
|
|
print(`Create a tenant user "${userName}", tenant: ${tenantId}`);
|
|
assert.commandWorked(db.getSiblingDB('$external').runCommand({
|
|
createUser: userName,
|
|
'$tenant': tenantId,
|
|
roles:
|
|
[{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}]
|
|
}));
|
|
|
|
adminDb.logout();
|
|
}
|
|
|
|
function prepareSecurityToken(conn) {
|
|
if (typeof conn._securityToken == 'undefined') {
|
|
print(`Inject security token to the connection: "${tojsononeline(conn)}", user: "${
|
|
kUserName}", tenant: ${kTenantId}`);
|
|
const securityToken =
|
|
_createSecurityToken({user: kUserName, db: '$external', tenant: kTenantId});
|
|
conn._setSecurityToken(securityToken);
|
|
}
|
|
}
|
|
|
|
function getDbName(nss) {
|
|
if (nss.length === 0 || !nss.includes(".")) {
|
|
return nss;
|
|
}
|
|
return nss.split(".")[0];
|
|
}
|
|
|
|
function checkDbNameInString(str, requestDbName, logError) {
|
|
if (requestDbName.length === 0) {
|
|
return;
|
|
}
|
|
if (requestDbName.includes("_")) {
|
|
return;
|
|
}
|
|
assert.eq(false, str.includes(kTenantId.str + "_" + requestDbName), logError);
|
|
}
|
|
|
|
function checkReponse(res, requestDbName, logError) {
|
|
// We expect the response db name matches request.
|
|
for (let k of Object.keys(res)) {
|
|
let v = res[k];
|
|
if (typeof v === "string") {
|
|
if (k === "dbName" || k == "db" || k == "dropped") {
|
|
checkDbNameInString(v, requestDbName, logError);
|
|
} else if (k === "namespace" || k === "ns") {
|
|
checkDbNameInString(getDbName(v), requestDbName, logError);
|
|
} else if (k === "errmsg" || k == "name") {
|
|
checkDbNameInString(v, requestDbName, logError);
|
|
}
|
|
} else if (Array.isArray(v)) {
|
|
v.forEach((item) => {
|
|
if (typeof item === "object" && item !== null)
|
|
checkReponse(item, requestDbName, logError);
|
|
});
|
|
} else if (typeof v === "object" && v !== null && Object.keys(v).length > 0) {
|
|
checkReponse(v, requestDbName, logError);
|
|
}
|
|
}
|
|
}
|
|
|
|
const kCmdsAllowedWithSecurityToken = new Set([
|
|
`abortTransaction`,
|
|
`aggregate`,
|
|
`availableQueryOptions`,
|
|
`buildinfo`,
|
|
`buildinfo`,
|
|
`collMod`,
|
|
`collStats`,
|
|
`collstats`,
|
|
`commitTransaction`,
|
|
`configureFailPoint`,
|
|
`count`,
|
|
`create`,
|
|
`createIndexes`,
|
|
`currentOp`,
|
|
`dbStats`,
|
|
`delete`,
|
|
`deleteIndexes`,
|
|
`distinct`,
|
|
`drop`,
|
|
`dropDatabase`,
|
|
`dropIndexes`,
|
|
`explain`,
|
|
`features`,
|
|
`filemd5`,
|
|
`find`,
|
|
`findAndModify`,
|
|
`findandmodify`,
|
|
`geoNear`,
|
|
`geoSearch`,
|
|
`getMore`,
|
|
`getParameter`,
|
|
`hello`,
|
|
`insert`,
|
|
`isMaster`,
|
|
`ismaster`,
|
|
`listCollections`,
|
|
`listCommands`,
|
|
`listDatabases`,
|
|
`listIndexes`,
|
|
`ping`,
|
|
`renameCollection`,
|
|
`resourceUsage`,
|
|
`rolesInfo`,
|
|
`serverStatus`,
|
|
`startSession`,
|
|
`statusMetrics`,
|
|
`update`,
|
|
`usersInfo`,
|
|
`validate`,
|
|
// The following commands are going to be allowed in serverless mode but stil not ready for
|
|
// per-tenant yet.
|
|
// `killCursors`,
|
|
// `connectionStatus`,
|
|
// `connPoolStats`,
|
|
// `top`,
|
|
`killop`,
|
|
// `endSessions`,
|
|
]);
|
|
|
|
function isAllowedWithSecurityToken(cmdName) {
|
|
return kCmdsAllowedWithSecurityToken.has(cmdName);
|
|
}
|
|
|
|
// Override the runCommand to inject security token and check for the response has the right nss and
|
|
// db name.
|
|
function runCommandWithResponseCheck(
|
|
conn, dbName, cmdName, cmdObj, originalRunCommand, makeRunCommandArgs) {
|
|
if (!isAllowedWithSecurityToken(cmdName)) {
|
|
throw new Error(
|
|
"Refusing to run a test that issues commands that are not allowed with security token, " +
|
|
" CmdName: " + cmdName + ", CmdObj: " + tojson(cmdObj));
|
|
}
|
|
|
|
prepareSecurityToken(conn);
|
|
|
|
// Actually run the provided command.
|
|
let res = originalRunCommand.apply(conn, makeRunCommandArgs(cmdObj));
|
|
const logUnmatchedDbName = (dbName, resObj) => {
|
|
return `Response db name does not match sent db name "${dbName}", response: ${
|
|
tojsononeline(resObj)}`;
|
|
};
|
|
|
|
checkReponse(res, dbName, logUnmatchedDbName(dbName, res));
|
|
return res;
|
|
}
|
|
|
|
createTenantUser(kUserName, kTenantId);
|
|
|
|
OverrideHelpers.prependOverrideInParallelShell(
|
|
"jstests/libs/override_methods/inject_security_token.js");
|
|
OverrideHelpers.overrideRunCommand(runCommandWithResponseCheck);
|
|
}());
|