diff options
author | jannaerin <golden.janna@gmail.com> | 2022-12-16 18:17:46 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-12-16 19:01:01 +0000 |
commit | 38dc821cbbdac94fa04e370f54140d26a272b507 (patch) | |
tree | fa83628160d543fe2afbc304f6f415efcb363d08 /jstests/serverless | |
parent | ce6fc190e3124c30b2dfb8a3acfed6a178b4ac30 (diff) | |
download | mongo-38dc821cbbdac94fa04e370f54140d26a272b507.tar.gz |
SERVER-71042 Remove featureFlagRequireTenantID from fully_disabled_feature_flags
Diffstat (limited to 'jstests/serverless')
6 files changed, 1177 insertions, 1257 deletions
diff --git a/jstests/serverless/list_databases_for_all_tenants.js b/jstests/serverless/list_databases_for_all_tenants.js index cc2a34f7bde..f24be8b53ad 100644 --- a/jstests/serverless/list_databases_for_all_tenants.js +++ b/jstests/serverless/list_databases_for_all_tenants.js @@ -228,7 +228,7 @@ function runTestInvalidCommands(primary) { ErrorCodes.Unauthorized); } -function runTestsWithMultiTenancySupport(featureFlagRequireTenantID) { +function runTestsWithMultiTenancySupport() { const rst = new ReplSetTest({ nodes: 2, nodeOptions: { @@ -236,7 +236,6 @@ function runTestsWithMultiTenancySupport(featureFlagRequireTenantID) { setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, - featureFlagRequireTenantID: featureFlagRequireTenantID } } }); @@ -267,7 +266,6 @@ function runTestNoMultiTenancySupport() { setParameter: { multitenancySupport: false, featureFlagSecurityToken: true, - featureFlagRequireTenantID: false } } }); @@ -287,7 +285,6 @@ function runTestNoMultiTenancySupport() { rst.stopSet(); } -runTestsWithMultiTenancySupport(true); -runTestsWithMultiTenancySupport(false); +runTestsWithMultiTenancySupport(); runTestNoMultiTenancySupport(); }()); diff --git a/jstests/serverless/native_tenant_data_isolation_basic_dollar_tenant.js b/jstests/serverless/native_tenant_data_isolation_basic_dollar_tenant.js index a464d9ec2e0..c1e91d759fb 100644 --- a/jstests/serverless/native_tenant_data_isolation_basic_dollar_tenant.js +++ b/jstests/serverless/native_tenant_data_isolation_basic_dollar_tenant.js @@ -4,479 +4,463 @@ "use strict"; load('jstests/aggregation/extras/utils.js'); // For arrayEq() - -function runTest(featureFlagRequireTenantId) { - const rst = new ReplSetTest({ - nodes: 3, - nodeOptions: { - auth: '', - setParameter: - {multitenancySupport: true, featureFlagRequireTenantID: featureFlagRequireTenantId} - } - }); - rst.startSet({keyFile: 'jstests/libs/key1'}); - rst.initiate(); - - const primary = rst.getPrimary(); - const adminDb = primary.getDB('admin'); - - // Prepare a user for testing pass tenant via $tenant. - // Must be authenticated as a user with ActionType::useTenant in order to use $tenant. - assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); - assert(adminDb.auth('admin', 'pwd')); - - const kTenant = ObjectId(); - const kOtherTenant = ObjectId(); - const kDbName = 'myDb'; - const kCollName = 'myColl'; - const testDb = primary.getDB(kDbName); - const testColl = testDb.getCollection(kCollName); - - // In this jstest, the collection (defined by kCollName) and the document "{_id: 0, a: 1, b: 1}" - // for the tenant (defined by kTenant) will be reused by all command tests. So, any test which - // changes the collection name or document should reset it. - - // Test create and listCollections commands, plus $listCatalog aggregate, on collection. - { - const viewName = "view1"; - const targetViews = 'system.views'; - - // Create a collection for the tenant kTenant, and then create a view on the collection. - assert.commandWorked( - testColl.getDB().createCollection(testColl.getName(), {'$tenant': kTenant})); - assert.commandWorked(testDb.runCommand( - {"create": viewName, "viewOn": kCollName, pipeline: [], '$tenant': kTenant})); - - const colls = assert.commandWorked( - testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kTenant})); - assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); - const expectedColls = [ - {"name": kCollName, "type": "collection"}, - {"name": targetViews, "type": "collection"}, - {"name": viewName, "type": "view"} - ]; - assert(arrayEq(expectedColls, colls.cursor.firstBatch), tojson(colls.cursor.firstBatch)); - - const prefixedDbName = kTenant + '_' + testDb.getName(); - const targetDb = featureFlagRequireTenantId ? testDb.getName() : prefixedDbName; - - // Get catalog without specifying target collection (collectionless). - let result = adminDb.runCommand( - {aggregate: 1, pipeline: [{$listCatalog: {}}], cursor: {}, '$tenant': kTenant}); - let resultArray = result.cursor.firstBatch; - - // Check that the resulting array of catalog entries contains our target databases and - // namespaces. - assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); - - // Also check that the resulting array contains views specific to our target database. - assert( - resultArray.some((entry) => (entry.db === targetDb) && (entry.name === targetViews))); - assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === viewName))); - - // Get catalog when specifying our target collection, which should only return one result. - result = testDb.runCommand({ - aggregate: testColl.getName(), - pipeline: [{$listCatalog: {}}], - cursor: {}, - '$tenant': kTenant - }); - resultArray = result.cursor.firstBatch; - - // Check that the resulting array of catalog entries contains our target database and - // namespace. - assert(resultArray.length == 1); - assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); - - // These collections should not be accessed with a different tenant. - const collsWithDiffTenant = assert.commandWorked( - testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kOtherTenant})); - assert.eq(0, collsWithDiffTenant.cursor.firstBatch.length); - } - - // Test listDatabases command. - { - // Create databases for kTenant. A new database is implicitly created when a collection is - // created. - const kOtherDbName = 'otherDb'; - assert.commandWorked( - primary.getDB(kOtherDbName).createCollection(kCollName, {'$tenant': kTenant})); - - const dbs = assert.commandWorked( - adminDb.runCommand({listDatabases: 1, nameOnly: true, '$tenant': kTenant})); - assert.eq(2, dbs.databases.length); - // The 'admin' database is not expected because we do not create a tenant user in this test. - const expectedDbs = featureFlagRequireTenantId - ? [kDbName, kOtherDbName] - : [kTenant + "_" + kDbName, kTenant + "_" + kOtherDbName]; - assert(arrayEq(expectedDbs, dbs.databases.map(db => db.name))); - - // These databases should not be accessed with a different tenant. - const dbsWithDiffTenant = assert.commandWorked( - adminDb.runCommand({listDatabases: 1, nameOnly: true, '$tenant': kOtherTenant})); - assert.eq(0, dbsWithDiffTenant.databases.length); - - const allDbs = assert.commandWorked(adminDb.runCommand({listDatabases: 1, nameOnly: true})); - expectedDbs.push("admin"); - expectedDbs.push("config"); - expectedDbs.push("local"); - - if (featureFlagRequireTenantId) { - assert.eq(0, allDbs.databases.length); - assert(arrayEq([], allDbs.databases.map(db => db.name))); - } else { - assert.eq(5, allDbs.databases.length); - assert(arrayEq(expectedDbs, allDbs.databases.map(db => db.name))); +load("jstests/libs/feature_flag_util.js"); // for isEnabled + +const rst = new ReplSetTest({ + nodes: 3, + nodeOptions: { + auth: '', + setParameter: { + multitenancySupport: true, } } +}); +rst.startSet({keyFile: 'jstests/libs/key1'}); +rst.initiate(); + +const primary = rst.getPrimary(); +const adminDb = primary.getDB('admin'); + +// Prepare a user for testing pass tenant via $tenant. +// Must be authenticated as a user with ActionType::useTenant in order to use $tenant. +assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +assert(adminDb.auth('admin', 'pwd')); + +const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTenantID"); + +const kTenant = ObjectId(); +const kOtherTenant = ObjectId(); +const kDbName = 'myDb'; +const kCollName = 'myColl'; +const testDb = primary.getDB(kDbName); +const testColl = testDb.getCollection(kCollName); + +// In this jstest, the collection (defined by kCollName) and the document "{_id: 0, a: 1, b: 1}" +// for the tenant (defined by kTenant) will be reused by all command tests. So, any test which +// changes the collection name or document should reset it. + +// Test create and listCollections commands, plus $listCatalog aggregate, on collection. +{ + const viewName = "view1"; + const targetViews = 'system.views'; + + // Create a collection for the tenant kTenant, and then create a view on the collection. + assert.commandWorked( + testColl.getDB().createCollection(testColl.getName(), {'$tenant': kTenant})); + assert.commandWorked(testDb.runCommand( + {"create": viewName, "viewOn": kCollName, pipeline: [], '$tenant': kTenant})); + + const colls = assert.commandWorked( + testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kTenant})); + assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); + const expectedColls = [ + {"name": kCollName, "type": "collection"}, + {"name": targetViews, "type": "collection"}, + {"name": viewName, "type": "view"} + ]; + assert(arrayEq(expectedColls, colls.cursor.firstBatch), tojson(colls.cursor.firstBatch)); + + const prefixedDbName = kTenant + '_' + testDb.getName(); + const targetDb = featureFlagRequireTenantId ? testDb.getName() : prefixedDbName; + + // Get catalog without specifying target collection (collectionless). + let result = adminDb.runCommand( + {aggregate: 1, pipeline: [{$listCatalog: {}}], cursor: {}, '$tenant': kTenant}); + let resultArray = result.cursor.firstBatch; + + // Check that the resulting array of catalog entries contains our target databases and + // namespaces. + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); + + // Also check that the resulting array contains views specific to our target database. + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === targetViews))); + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === viewName))); + + // Get catalog when specifying our target collection, which should only return one result. + result = testDb.runCommand({ + aggregate: testColl.getName(), + pipeline: [{$listCatalog: {}}], + cursor: {}, + '$tenant': kTenant + }); + resultArray = result.cursor.firstBatch; - // Test insert, agg, find, getMore, and explain commands. - { - const kTenantDocs = [{w: 0}, {x: 1}, {y: 2}, {z: 3}]; - const kOtherTenantDocs = [{i: 1}, {j: 2}, {k: 3}]; - - assert.commandWorked( - testDb.runCommand({insert: kCollName, documents: kTenantDocs, '$tenant': kTenant})); - assert.commandWorked(testDb.runCommand( - {insert: kCollName, documents: kOtherTenantDocs, '$tenant': kOtherTenant})); - - // Check that find only returns documents from the correct tenant - const findRes = assert.commandWorked( - testDb.runCommand({find: kCollName, projection: {_id: 0}, '$tenant': kTenant})); - assert.eq(kTenantDocs.length, - findRes.cursor.firstBatch.length, - tojson(findRes.cursor.firstBatch)); - assert(arrayEq(kTenantDocs, findRes.cursor.firstBatch), tojson(findRes.cursor.firstBatch)); - - const findRes2 = assert.commandWorked( - testDb.runCommand({find: kCollName, projection: {_id: 0}, '$tenant': kOtherTenant})); - assert.eq(kOtherTenantDocs.length, - findRes2.cursor.firstBatch.length, - tojson(findRes2.cursor.firstBatch)); - assert(arrayEq(kOtherTenantDocs, findRes2.cursor.firstBatch), - tojson(findRes2.cursor.firstBatch)); - - // Test that getMore only works on a tenant's own cursor - const cmdRes = assert.commandWorked(testDb.runCommand( - {find: kCollName, projection: {_id: 0}, batchSize: 1, '$tenant': kTenant})); - assert.eq(cmdRes.cursor.firstBatch.length, 1, tojson(cmdRes.cursor.firstBatch)); - assert.commandWorked(testDb.runCommand( - {getMore: cmdRes.cursor.id, collection: kCollName, '$tenant': kTenant})); - - const cmdRes2 = assert.commandWorked(testDb.runCommand( - {find: kCollName, projection: {_id: 0}, batchSize: 1, '$tenant': kTenant})); - assert.commandFailedWithCode( - testDb.runCommand( - {getMore: cmdRes2.cursor.id, collection: kCollName, '$tenant': kOtherTenant}), - ErrorCodes.Unauthorized); - - // Test that aggregate only finds a tenant's own document. - const aggRes = assert.commandWorked(testDb.runCommand({ - aggregate: kCollName, - pipeline: [{$match: {w: 0}}, {$project: {_id: 0}}], - cursor: {}, - '$tenant': kTenant - })); - assert.eq(1, aggRes.cursor.firstBatch.length, tojson(aggRes.cursor.firstBatch)); - assert.eq(kTenantDocs[0], aggRes.cursor.firstBatch[0]); - - const aggRes2 = assert.commandWorked(testDb.runCommand({ - aggregate: kCollName, - pipeline: [{$match: {i: 1}}, {$project: {_id: 0}}], - cursor: {}, - '$tenant': kOtherTenant - })); - assert.eq(1, aggRes2.cursor.firstBatch.length, tojson(aggRes2.cursor.firstBatch)); - assert.eq(kOtherTenantDocs[0], aggRes2.cursor.firstBatch[0]); - - // Test that explain works correctly. - const kTenantExplainRes = assert.commandWorked(testDb.runCommand( - {explain: {find: kCollName}, verbosity: 'executionStats', '$tenant': kTenant})); - assert.eq(kTenantDocs.length, - kTenantExplainRes.executionStats.nReturned, - tojson(kTenantExplainRes)); - const kOtherTenantExplainRes = assert.commandWorked(testDb.runCommand( - {explain: {find: kCollName}, verbosity: 'executionStats', '$tenant': kOtherTenant})); - assert.eq(kOtherTenantDocs.length, - kOtherTenantExplainRes.executionStats.nReturned, - tojson(kOtherTenantExplainRes)); - } - - // Test insert and findAndModify command. - { - assert.commandWorked(testDb.runCommand( - {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); + // Check that the resulting array of catalog entries contains our target database and + // namespace. + assert(resultArray.length == 1); + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); - const fad1 = assert.commandWorked(testDb.runCommand({ - findAndModify: kCollName, - query: {a: 1}, - update: {$inc: {a: 10}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - const fad2 = assert.commandWorked(testDb.runCommand({ - findAndModify: kCollName, - query: {a: 11}, - update: {$set: {a: 1, b: 1}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 11, b: 1}, fad2.value); - // This document should not be accessed with a different tenant. - const fadOtherUser = assert.commandWorked(testDb.runCommand({ - findAndModify: kCollName, - query: {b: 1}, - update: {$inc: {b: 10}}, - '$tenant': kOtherTenant - })); - assert.eq(null, fadOtherUser.value); - } + // These collections should not be accessed with a different tenant. + const collsWithDiffTenant = assert.commandWorked( + testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kOtherTenant})); + assert.eq(0, collsWithDiffTenant.cursor.firstBatch.length); +} - // Test count and distinct command. - { - assert.commandWorked(testDb.runCommand( - {insert: kCollName, documents: [{c: 1, d: 1}, {c: 1, d: 2}], '$tenant': kTenant})); - - // Test count command. - const resCount = assert.commandWorked( - testDb.runCommand({count: kCollName, query: {c: 1}, '$tenant': kTenant})); - assert.eq(2, resCount.n); - const resCountOtherUser = assert.commandWorked( - testDb.runCommand({count: kCollName, query: {c: 1}, '$tenant': kOtherTenant})); - assert.eq(0, resCountOtherUser.n); - - // Test Distict command. - const resDistinct = assert.commandWorked( - testDb.runCommand({distinct: kCollName, key: 'd', query: {}, '$tenant': kTenant})); - assert.eq([1, 2], resDistinct.values.sort()); - const resDistinctOtherUser = assert.commandWorked( - testDb.runCommand({distinct: kCollName, key: 'd', query: {}, '$tenant': kOtherTenant})); - assert.eq([], resDistinctOtherUser.values); +// Test listDatabases command. +{ + // Create databases for kTenant. A new database is implicitly created when a collection is + // created. + const kOtherDbName = 'otherDb'; + assert.commandWorked( + primary.getDB(kOtherDbName).createCollection(kCollName, {'$tenant': kTenant})); + + const dbs = assert.commandWorked( + adminDb.runCommand({listDatabases: 1, nameOnly: true, '$tenant': kTenant})); + assert.eq(2, dbs.databases.length); + // The 'admin' database is not expected because we do not create a tenant user in this test. + const expectedDbs = featureFlagRequireTenantId + ? [kDbName, kOtherDbName] + : [kTenant + "_" + kDbName, kTenant + "_" + kOtherDbName]; + assert(arrayEq(expectedDbs, dbs.databases.map(db => db.name))); + + // These databases should not be accessed with a different tenant. + const dbsWithDiffTenant = assert.commandWorked( + adminDb.runCommand({listDatabases: 1, nameOnly: true, '$tenant': kOtherTenant})); + assert.eq(0, dbsWithDiffTenant.databases.length); + + const allDbs = assert.commandWorked(adminDb.runCommand({listDatabases: 1, nameOnly: true})); + expectedDbs.push("admin"); + expectedDbs.push("config"); + expectedDbs.push("local"); + + if (featureFlagRequireTenantId) { + assert.eq(0, allDbs.databases.length); + assert(arrayEq([], allDbs.databases.map(db => db.name))); + } else { + assert.eq(5, allDbs.databases.length); + assert(arrayEq(expectedDbs, allDbs.databases.map(db => db.name))); } +} - // Test renameCollection command. - { - const fromName = kDbName + "." + kCollName; - const toName = fromName + "_renamed"; - assert.commandWorked(adminDb.runCommand( - {renameCollection: fromName, to: toName, dropTarget: true, '$tenant': kTenant})); - - // Verify the the renamed collection by findAndModify existing documents. - const fad1 = assert.commandWorked(testDb.runCommand({ - findAndModify: kCollName + "_renamed", - query: {a: 1}, - update: {$inc: {a: 10}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - - // This collection should not be accessed with a different tenant. - assert.commandFailedWithCode(adminDb.runCommand({ - renameCollection: toName, - to: fromName, - dropTarget: true, - '$tenant': kOtherTenant - }), - ErrorCodes.NamespaceNotFound); - - // Reset the collection to be used below - assert.commandWorked(adminDb.runCommand( - {renameCollection: toName, to: fromName, dropTarget: true, '$tenant': kTenant})); - } +// Test insert, agg, find, getMore, and explain commands. +{ + const kTenantDocs = [{w: 0}, {x: 1}, {y: 2}, {z: 3}]; + const kOtherTenantDocs = [{i: 1}, {j: 2}, {k: 3}]; + + assert.commandWorked( + testDb.runCommand({insert: kCollName, documents: kTenantDocs, '$tenant': kTenant})); + assert.commandWorked(testDb.runCommand( + {insert: kCollName, documents: kOtherTenantDocs, '$tenant': kOtherTenant})); + + // Check that find only returns documents from the correct tenant + const findRes = assert.commandWorked( + testDb.runCommand({find: kCollName, projection: {_id: 0}, '$tenant': kTenant})); + assert.eq( + kTenantDocs.length, findRes.cursor.firstBatch.length, tojson(findRes.cursor.firstBatch)); + assert(arrayEq(kTenantDocs, findRes.cursor.firstBatch), tojson(findRes.cursor.firstBatch)); + + const findRes2 = assert.commandWorked( + testDb.runCommand({find: kCollName, projection: {_id: 0}, '$tenant': kOtherTenant})); + assert.eq(kOtherTenantDocs.length, + findRes2.cursor.firstBatch.length, + tojson(findRes2.cursor.firstBatch)); + assert(arrayEq(kOtherTenantDocs, findRes2.cursor.firstBatch), + tojson(findRes2.cursor.firstBatch)); + + // Test that getMore only works on a tenant's own cursor + const cmdRes = assert.commandWorked(testDb.runCommand( + {find: kCollName, projection: {_id: 0}, batchSize: 1, '$tenant': kTenant})); + assert.eq(cmdRes.cursor.firstBatch.length, 1, tojson(cmdRes.cursor.firstBatch)); + assert.commandWorked( + testDb.runCommand({getMore: cmdRes.cursor.id, collection: kCollName, '$tenant': kTenant})); + + const cmdRes2 = assert.commandWorked(testDb.runCommand( + {find: kCollName, projection: {_id: 0}, batchSize: 1, '$tenant': kTenant})); + assert.commandFailedWithCode( + testDb.runCommand( + {getMore: cmdRes2.cursor.id, collection: kCollName, '$tenant': kOtherTenant}), + ErrorCodes.Unauthorized); + + // Test that aggregate only finds a tenant's own document. + const aggRes = assert.commandWorked(testDb.runCommand({ + aggregate: kCollName, + pipeline: [{$match: {w: 0}}, {$project: {_id: 0}}], + cursor: {}, + '$tenant': kTenant + })); + assert.eq(1, aggRes.cursor.firstBatch.length, tojson(aggRes.cursor.firstBatch)); + assert.eq(kTenantDocs[0], aggRes.cursor.firstBatch[0]); + + const aggRes2 = assert.commandWorked(testDb.runCommand({ + aggregate: kCollName, + pipeline: [{$match: {i: 1}}, {$project: {_id: 0}}], + cursor: {}, + '$tenant': kOtherTenant + })); + assert.eq(1, aggRes2.cursor.firstBatch.length, tojson(aggRes2.cursor.firstBatch)); + assert.eq(kOtherTenantDocs[0], aggRes2.cursor.firstBatch[0]); + + // Test that explain works correctly. + const kTenantExplainRes = assert.commandWorked(testDb.runCommand( + {explain: {find: kCollName}, verbosity: 'executionStats', '$tenant': kTenant})); + assert.eq( + kTenantDocs.length, kTenantExplainRes.executionStats.nReturned, tojson(kTenantExplainRes)); + const kOtherTenantExplainRes = assert.commandWorked(testDb.runCommand( + {explain: {find: kCollName}, verbosity: 'executionStats', '$tenant': kOtherTenant})); + assert.eq(kOtherTenantDocs.length, + kOtherTenantExplainRes.executionStats.nReturned, + tojson(kOtherTenantExplainRes)); +} - // Test the dropCollection and dropDatabase commands. - { - // Another tenant shouldn't be able to drop the collection or database. - assert.commandWorked(testDb.runCommand({drop: kCollName, '$tenant': kOtherTenant})); - const collsAfterDropCollectionByOtherTenant = assert.commandWorked(testDb.runCommand( - {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); - assert.eq(1, - collsAfterDropCollectionByOtherTenant.cursor.firstBatch.length, - tojson(collsAfterDropCollectionByOtherTenant.cursor.firstBatch)); - - assert.commandWorked(testDb.runCommand({dropDatabase: 1, '$tenant': kOtherTenant})); - const collsAfterDropDbByOtherTenant = assert.commandWorked(testDb.runCommand( - {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); - assert.eq(1, - collsAfterDropDbByOtherTenant.cursor.firstBatch.length, - tojson(collsAfterDropDbByOtherTenant.cursor.firstBatch)); - - // Now, drop the collection using the original tenantId. - assert.commandWorked(testDb.runCommand({drop: kCollName, '$tenant': kTenant})); - const collsAfterDropCollection = assert.commandWorked(testDb.runCommand( - {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); - assert.eq(0, - collsAfterDropCollection.cursor.firstBatch.length, - tojson(collsAfterDropCollection.cursor.firstBatch)); - - // Now, drop the database using the original tenantId. - assert.commandWorked(testDb.runCommand({dropDatabase: 1, '$tenant': kTenant})); - const collsAfterDropDb = assert.commandWorked(testDb.runCommand( - {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); - assert.eq(0, - collsAfterDropDb.cursor.firstBatch.length, - tojson(collsAfterDropDb.cursor.firstBatch)); - - // Reset the collection so other test cases can still access this collection with kCollName - // after this test. - assert.commandWorked(testDb.runCommand( - {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); - } +// Test insert and findAndModify command. +{ + assert.commandWorked(testDb.runCommand( + {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); + + const fad1 = assert.commandWorked(testDb.runCommand( + {findAndModify: kCollName, query: {a: 1}, update: {$inc: {a: 10}}, '$tenant': kTenant})); + assert.eq({_id: 0, a: 1, b: 1}, fad1.value); + const fad2 = assert.commandWorked(testDb.runCommand({ + findAndModify: kCollName, + query: {a: 11}, + update: {$set: {a: 1, b: 1}}, + '$tenant': kTenant + })); + assert.eq({_id: 0, a: 11, b: 1}, fad2.value); + // This document should not be accessed with a different tenant. + const fadOtherUser = assert.commandWorked(testDb.runCommand({ + findAndModify: kCollName, + query: {b: 1}, + update: {$inc: {b: 10}}, + '$tenant': kOtherTenant + })); + assert.eq(null, fadOtherUser.value); +} - // Test that transactions can be run successfully. - { - const lsid = - assert.commandWorked(testDb.runCommand({startSession: 1, $tenant: kTenant})).id; - assert.commandWorked(testDb.runCommand({ - delete: kCollName, - deletes: [{q: {_id: 0, a: 1, b: 1}, limit: 1}], - startTransaction: true, - lsid: lsid, - txnNumber: NumberLong(0), - autocommit: false, - '$tenant': kTenant - })); - assert.commandWorked(testDb.adminCommand({ - commitTransaction: 1, - lsid: lsid, - txnNumber: NumberLong(0), - autocommit: false, - $tenant: kTenant - })); +// Test count and distinct command. +{ + assert.commandWorked(testDb.runCommand( + {insert: kCollName, documents: [{c: 1, d: 1}, {c: 1, d: 2}], '$tenant': kTenant})); + + // Test count command. + const resCount = assert.commandWorked( + testDb.runCommand({count: kCollName, query: {c: 1}, '$tenant': kTenant})); + assert.eq(2, resCount.n); + const resCountOtherUser = assert.commandWorked( + testDb.runCommand({count: kCollName, query: {c: 1}, '$tenant': kOtherTenant})); + assert.eq(0, resCountOtherUser.n); + + // Test Distict command. + const resDistinct = assert.commandWorked( + testDb.runCommand({distinct: kCollName, key: 'd', query: {}, '$tenant': kTenant})); + assert.eq([1, 2], resDistinct.values.sort()); + const resDistinctOtherUser = assert.commandWorked( + testDb.runCommand({distinct: kCollName, key: 'd', query: {}, '$tenant': kOtherTenant})); + assert.eq([], resDistinctOtherUser.values); +} - const findRes = - assert.commandWorked(testDb.runCommand({find: kCollName, '$tenant': kTenant})); - assert.eq(0, findRes.cursor.firstBatch.length, tojson(findRes.cursor.firstBatch)); +// Test renameCollection command. +{ + const fromName = kDbName + "." + kCollName; + const toName = fromName + "_renamed"; + assert.commandWorked(adminDb.runCommand( + {renameCollection: fromName, to: toName, dropTarget: true, '$tenant': kTenant})); + + // Verify the the renamed collection by findAndModify existing documents. + const fad1 = assert.commandWorked(testDb.runCommand({ + findAndModify: kCollName + "_renamed", + query: {a: 1}, + update: {$inc: {a: 10}}, + '$tenant': kTenant + })); + assert.eq({_id: 0, a: 1, b: 1}, fad1.value); + + // This collection should not be accessed with a different tenant. + assert.commandFailedWithCode( + adminDb.runCommand( + {renameCollection: toName, to: fromName, dropTarget: true, '$tenant': kOtherTenant}), + ErrorCodes.NamespaceNotFound); + + // Reset the collection to be used below + assert.commandWorked(adminDb.runCommand( + {renameCollection: toName, to: fromName, dropTarget: true, '$tenant': kTenant})); +} - // Reset the collection so other test cases can still access this collection with kCollName - // after this test. - assert.commandWorked(testDb.runCommand( - {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); - } +// Test the dropCollection and dropDatabase commands. +{ + // Another tenant shouldn't be able to drop the collection or database. + assert.commandWorked(testDb.runCommand({drop: kCollName, '$tenant': kOtherTenant})); + const collsAfterDropCollectionByOtherTenant = assert.commandWorked(testDb.runCommand( + {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); + assert.eq(1, + collsAfterDropCollectionByOtherTenant.cursor.firstBatch.length, + tojson(collsAfterDropCollectionByOtherTenant.cursor.firstBatch)); + + assert.commandWorked(testDb.runCommand({dropDatabase: 1, '$tenant': kOtherTenant})); + const collsAfterDropDbByOtherTenant = assert.commandWorked(testDb.runCommand( + {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); + assert.eq(1, + collsAfterDropDbByOtherTenant.cursor.firstBatch.length, + tojson(collsAfterDropDbByOtherTenant.cursor.firstBatch)); + + // Now, drop the collection using the original tenantId. + assert.commandWorked(testDb.runCommand({drop: kCollName, '$tenant': kTenant})); + const collsAfterDropCollection = assert.commandWorked(testDb.runCommand( + {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); + assert.eq(0, + collsAfterDropCollection.cursor.firstBatch.length, + tojson(collsAfterDropCollection.cursor.firstBatch)); + + // Now, drop the database using the original tenantId. + assert.commandWorked(testDb.runCommand({dropDatabase: 1, '$tenant': kTenant})); + const collsAfterDropDb = assert.commandWorked(testDb.runCommand( + {listCollections: 1, nameOnly: true, filter: {name: kCollName}, '$tenant': kTenant})); + assert.eq( + 0, collsAfterDropDb.cursor.firstBatch.length, tojson(collsAfterDropDb.cursor.firstBatch)); + + // Reset the collection so other test cases can still access this collection with kCollName + // after this test. + assert.commandWorked(testDb.runCommand( + {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); +} - // Test createIndexes, listIndexes and dropIndexes command. - { - var sortIndexesByName = function(indexes) { - return indexes.sort(function(a, b) { - return a.name > b.name; - }); - }; - - var getIndexesKeyAndName = function(indexes) { - return sortIndexesByName(indexes).map(function(index) { - return {key: index.key, name: index.name}; - }); - }; - - let res = assert.commandWorked(testDb.runCommand({ - createIndexes: kCollName, - indexes: [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}], - '$tenant': kTenant - })); - assert.eq(3, res.numIndexesAfter); - - res = assert.commandWorked(testDb.runCommand({listIndexes: kCollName, '$tenant': kTenant})); - assert.eq(3, res.cursor.firstBatch.length); - assert(arrayEq( - [ - {key: {"_id": 1}, name: "_id_"}, - {key: {a: 1}, name: "indexA"}, - {key: {b: 1}, name: "indexB"} - ], - getIndexesKeyAndName(res.cursor.firstBatch))); - - // These indexes should not be accessed with a different tenant. - assert.commandFailedWithCode( - testDb.runCommand({listIndexes: kCollName, '$tenant': kOtherTenant}), - ErrorCodes.NamespaceNotFound); - assert.commandFailedWithCode( - testDb.runCommand( - {dropIndexes: kCollName, index: ["indexA", "indexB"], '$tenant': kOtherTenant}), - ErrorCodes.NamespaceNotFound); - - // Drop those new created indexes. - res = assert.commandWorked(testDb.runCommand( - {dropIndexes: kCollName, index: ["indexA", "indexB"], '$tenant': kTenant})); - - res = assert.commandWorked(testDb.runCommand({listIndexes: kCollName, '$tenant': kTenant})); - assert.eq(1, res.cursor.firstBatch.length); - assert(arrayEq([{key: {"_id": 1}, name: "_id_"}], - getIndexesKeyAndName(res.cursor.firstBatch))); - } +// Test that transactions can be run successfully. +{ + const lsid = assert.commandWorked(testDb.runCommand({startSession: 1, $tenant: kTenant})).id; + assert.commandWorked(testDb.runCommand({ + delete: kCollName, + deletes: [{q: {_id: 0, a: 1, b: 1}, limit: 1}], + startTransaction: true, + lsid: lsid, + txnNumber: NumberLong(0), + autocommit: false, + '$tenant': kTenant + })); + assert.commandWorked(testDb.adminCommand({ + commitTransaction: 1, + lsid: lsid, + txnNumber: NumberLong(0), + autocommit: false, + $tenant: kTenant + })); + + const findRes = assert.commandWorked(testDb.runCommand({find: kCollName, '$tenant': kTenant})); + assert.eq(0, findRes.cursor.firstBatch.length, tojson(findRes.cursor.firstBatch)); + + // Reset the collection so other test cases can still access this collection with kCollName + // after this test. + assert.commandWorked(testDb.runCommand( + {insert: kCollName, documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); +} - // Test collMod - { - // Create the index used for collMod - let res = assert.commandWorked(testDb.runCommand({ - createIndexes: kCollName, - indexes: [{key: {c: 1}, name: "indexC", expireAfterSeconds: 50}], - '$tenant': kTenant - })); - assert.eq(2, res.numIndexesAfter); +// Test createIndexes, listIndexes and dropIndexes command. +{ + var sortIndexesByName = function(indexes) { + return indexes.sort(function(a, b) { + return a.name > b.name; + }); + }; - // Modifying the index without the tenantId should not work. - res = testDb.runCommand({ - "collMod": kCollName, - "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}, + var getIndexesKeyAndName = function(indexes) { + return sortIndexesByName(indexes).map(function(index) { + return {key: index.key, name: index.name}; }); + }; + + let res = assert.commandWorked(testDb.runCommand({ + createIndexes: kCollName, + indexes: [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}], + '$tenant': kTenant + })); + assert.eq(3, res.numIndexesAfter); + + res = assert.commandWorked(testDb.runCommand({listIndexes: kCollName, '$tenant': kTenant})); + assert.eq(3, res.cursor.firstBatch.length); + assert(arrayEq( + [ + {key: {"_id": 1}, name: "_id_"}, + {key: {a: 1}, name: "indexA"}, + {key: {b: 1}, name: "indexB"} + ], + getIndexesKeyAndName(res.cursor.firstBatch))); + + // These indexes should not be accessed with a different tenant. + assert.commandFailedWithCode( + testDb.runCommand({listIndexes: kCollName, '$tenant': kOtherTenant}), + ErrorCodes.NamespaceNotFound); + assert.commandFailedWithCode( + testDb.runCommand( + {dropIndexes: kCollName, index: ["indexA", "indexB"], '$tenant': kOtherTenant}), + ErrorCodes.NamespaceNotFound); + + // Drop those new created indexes. + res = assert.commandWorked(testDb.runCommand( + {dropIndexes: kCollName, index: ["indexA", "indexB"], '$tenant': kTenant})); + + res = assert.commandWorked(testDb.runCommand({listIndexes: kCollName, '$tenant': kTenant})); + assert.eq(1, res.cursor.firstBatch.length); + assert(arrayEq([{key: {"_id": 1}, name: "_id_"}], getIndexesKeyAndName(res.cursor.firstBatch))); +} + +// Test collMod +{ + // Create the index used for collMod + let res = assert.commandWorked(testDb.runCommand({ + createIndexes: kCollName, + indexes: [{key: {c: 1}, name: "indexC", expireAfterSeconds: 50}], + '$tenant': kTenant + })); + assert.eq(2, res.numIndexesAfter); + + // Modifying the index without the tenantId should not work. + res = testDb.runCommand({ + "collMod": kCollName, + "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}, + }); + assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound); + // TODO SERVER-70876 Uncomment out this conditional below, and remove the assertion above in + // favor of the assertion in the conditional. + /* if (featureFlagRequireTenantId) { + assert.commandFailedWithCode(res, 7005300); + } else { assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound); - // TODO SERVER-70876 Uncomment out this conditional below, and remove the assertion above in - // favor of the assertion in the conditional. - /* if (featureFlagRequireTenantId) { - assert.commandFailedWithCode(res, 7005300); - } else { - assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound); - } - */ + } + */ + + // Modify the index with the tenantId + res = assert.commandWorked(testDb.runCommand({ + "collMod": kCollName, + "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}, + '$tenant': kTenant + })); + assert.eq(50, res.expireAfterSeconds_old); + assert.eq(100, res.expireAfterSeconds_new); + + // Drop the index created + assert.commandWorked( + testDb.runCommand({dropIndexes: kCollName, index: ["indexC"], '$tenant': kTenant})); +} - // Modify the index with the tenantId - res = assert.commandWorked(testDb.runCommand({ - "collMod": kCollName, - "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}, +// Test the applyOps command +{ + if (featureFlagRequireTenantId) { + assert.commandWorked(testDb.runCommand({ + applyOps: + [{"op": "i", "ns": testColl.getFullName(), "tid": kTenant, "o": {_id: 5, x: 17}}], '$tenant': kTenant })); - assert.eq(50, res.expireAfterSeconds_old); - assert.eq(100, res.expireAfterSeconds_new); - - // Drop the index created - assert.commandWorked( - testDb.runCommand({dropIndexes: kCollName, index: ["indexC"], '$tenant': kTenant})); - } - - // Test the applyOps command - { - if (featureFlagRequireTenantId) { - assert.commandWorked(testDb.runCommand({ - applyOps: [ - {"op": "i", "ns": testColl.getFullName(), "tid": kTenant, "o": {_id: 5, x: 17}} - ], - '$tenant': kTenant - })); - } else { - const ns = kTenant + '_' + testColl.getFullName(); - assert.commandWorked(testDb.runCommand( - {applyOps: [{"op": "i", "ns": ns, "o": {_id: 5, x: 17}}], '$tenant': kTenant})); - } - - // Check applyOp inserted the document. - const findRes = assert.commandWorked( - testDb.runCommand({find: kCollName, filter: {_id: 5}, '$tenant': kTenant})); - assert.eq(1, findRes.cursor.firstBatch.length); - assert.eq(17, findRes.cursor.firstBatch[0].x); - } - - // Test the validate command. - { - const validateRes = - assert.commandWorked(testDb.runCommand({validate: kCollName, '$tenant': kTenant})); - assert(validateRes.valid); + } else { + const ns = kTenant + '_' + testColl.getFullName(); + assert.commandWorked(testDb.runCommand( + {applyOps: [{"op": "i", "ns": ns, "o": {_id: 5, x: 17}}], '$tenant': kTenant})); } - // Test dbCheck command. - { assert.commandWorked(testDb.runCommand({dbCheck: kCollName, '$tenant': kTenant})); } + // Check applyOp inserted the document. + const findRes = assert.commandWorked( + testDb.runCommand({find: kCollName, filter: {_id: 5}, '$tenant': kTenant})); + assert.eq(1, findRes.cursor.firstBatch.length); + assert.eq(17, findRes.cursor.firstBatch[0].x); +} - rst.stopSet(); +// Test the validate command. +{ + const validateRes = + assert.commandWorked(testDb.runCommand({validate: kCollName, '$tenant': kTenant})); + assert(validateRes.valid); } -runTest(true); -runTest(false); +// Test dbCheck command. +{ assert.commandWorked(testDb.runCommand({dbCheck: kCollName, '$tenant': kTenant})); } + +rst.stopSet(); })(); diff --git a/jstests/serverless/native_tenant_data_isolation_basic_security_token.js b/jstests/serverless/native_tenant_data_isolation_basic_security_token.js index 52659cf126a..62502f7ca39 100644 --- a/jstests/serverless/native_tenant_data_isolation_basic_security_token.js +++ b/jstests/serverless/native_tenant_data_isolation_basic_security_token.js @@ -4,6 +4,7 @@ "use strict"; load('jstests/aggregation/extras/utils.js'); // For arrayEq() +load("jstests/libs/feature_flag_util.js"); // for isEnabled function checkNsSerializedCorrectly( featureFlagRequireTenantId, kTenant, kDbName, kCollectionName, nsField) { @@ -20,510 +21,486 @@ function checkNsSerializedCorrectly( } } -function runTest(featureFlagRequireTenantId) { - const rst = new ReplSetTest({ - nodes: 3, - nodeOptions: { - auth: '', - setParameter: { - multitenancySupport: true, - featureFlagSecurityToken: true, - featureFlagRequireTenantID: featureFlagRequireTenantId - } +const rst = new ReplSetTest({ + nodes: 3, + nodeOptions: { + auth: '', + setParameter: { + multitenancySupport: true, + featureFlagSecurityToken: true, } - }); - rst.startSet({keyFile: 'jstests/libs/key1'}); - rst.initiate(); - - const primary = rst.getPrimary(); - const adminDb = primary.getDB('admin'); - - // Prepare a user for testing pass tenant via $tenant. - // Must be authenticated as a user with ActionType::useTenant in order to use $tenant. - assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); - assert(adminDb.auth('admin', 'pwd')); - - const kTenant = ObjectId(); - const kOtherTenant = ObjectId(); - const kDbName = 'myDb'; - const kCollName = 'myColl'; - const tokenConn = new Mongo(primary.host); - const securityToken = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); - const tokenDB = tokenConn.getDB(kDbName); - - // In this jstest, the collection (defined by kCollName) and the document "{_id: 0, a: 1, b: 1}" - // for the tenant (defined by kTenant) will be reused by all command tests. So, any test which - // changes the collection name or document should reset it. - - // Test commands using a security token for one tenant. + } +}); +rst.startSet({keyFile: 'jstests/libs/key1'}); +rst.initiate(); + +const primary = rst.getPrimary(); +const adminDb = primary.getDB('admin'); + +// Prepare a user for testing pass tenant via $tenant. +// Must be authenticated as a user with ActionType::useTenant in order to use $tenant. +assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +assert(adminDb.auth('admin', 'pwd')); + +const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled( + adminDb, + "RequireTenantID", +); + +const kTenant = ObjectId(); +const kOtherTenant = ObjectId(); +const kDbName = 'myDb'; +const kCollName = 'myColl'; +const tokenConn = new Mongo(primary.host); +const securityToken = _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); +const tokenDB = tokenConn.getDB(kDbName); + +// In this jstest, the collection (defined by kCollName) and the document "{_id: 0, a: 1, b: 1}" +// for the tenant (defined by kTenant) will be reused by all command tests. So, any test which +// changes the collection name or document should reset it. + +// Test commands using a security token for one tenant. +{ + // Create a user for kTenant and then set the security token on the connection. + assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant1", + '$tenant': kTenant, + roles: + [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] + })); + + tokenConn._setSecurityToken(securityToken); + + // Logout the root user to avoid multiple authentication. + tokenConn.getDB("admin").logout(); + + // Create a collection for the tenant kTenant and then insert into it. + assert.commandWorked(tokenDB.createCollection(kCollName)); + assert.commandWorked( + tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); + + // Find the document and call getMore on the cursor { - // Create a user for kTenant and then set the security token on the connection. - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant1", - '$tenant': kTenant, - roles: [ - {role: 'dbAdminAnyDatabase', db: 'admin'}, - {role: 'readWriteAnyDatabase', db: 'admin'} - ] - })); - - tokenConn._setSecurityToken(securityToken); - - // Logout the root user to avoid multiple authentication. - tokenConn.getDB("admin").logout(); - - // Create a collection for the tenant kTenant and then insert into it. - assert.commandWorked(tokenDB.createCollection(kCollName)); + // Add additional document in order to have two batches for getMore assert.commandWorked( - tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); + tokenDB.runCommand({insert: kCollName, documents: [{_id: 100, x: 1}]})); + + const findRes = assert.commandWorked( + tokenDB.runCommand({find: kCollName, filter: {a: 1}, batchSize: 1})); + assert(arrayEq([{_id: 0, a: 1, b: 1}], findRes.cursor.firstBatch), tojson(findRes)); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, findRes.cursor.ns); + + const getMoreRes = assert.commandWorked( + tokenDB.runCommand({getMore: findRes.cursor.id, collection: kCollName})); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, getMoreRes.cursor.ns); + } - // Find the document and call getMore on the cursor - { - // Add additional document in order to have two batches for getMore - assert.commandWorked( - tokenDB.runCommand({insert: kCollName, documents: [{_id: 100, x: 1}]})); - - const findRes = assert.commandWorked( - tokenDB.runCommand({find: kCollName, filter: {a: 1}, batchSize: 1})); - assert(arrayEq([{_id: 0, a: 1, b: 1}], findRes.cursor.firstBatch), tojson(findRes)); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, findRes.cursor.ns); - - const getMoreRes = assert.commandWorked( - tokenDB.runCommand({getMore: findRes.cursor.id, collection: kCollName})); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, getMoreRes.cursor.ns); - } + // Test the aggregate command. + { + const aggRes = assert.commandWorked( + tokenDB.runCommand({aggregate: kCollName, pipeline: [{$match: {a: 1}}], cursor: {}})); + assert(arrayEq([{_id: 0, a: 1, b: 1}], aggRes.cursor.firstBatch), tojson(aggRes)); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, aggRes.cursor.ns); + } - // Test the aggregate command. - { - const aggRes = assert.commandWorked(tokenDB.runCommand( - {aggregate: kCollName, pipeline: [{$match: {a: 1}}], cursor: {}})); - assert(arrayEq([{_id: 0, a: 1, b: 1}], aggRes.cursor.firstBatch), tojson(aggRes)); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, aggRes.cursor.ns); - } + // Find and modify the document. + { + const fad1 = assert.commandWorked( + tokenDB.runCommand({findAndModify: kCollName, query: {a: 1}, update: {$inc: {a: 10}}})); + assert.eq({_id: 0, a: 1, b: 1}, fad1.value); + const fad2 = assert.commandWorked(tokenDB.runCommand( + {findAndModify: kCollName, query: {a: 11}, update: {$set: {a: 1, b: 1}}})); + assert.eq({_id: 0, a: 11, b: 1}, fad2.value); + } - // Find and modify the document. - { - const fad1 = assert.commandWorked(tokenDB.runCommand( - {findAndModify: kCollName, query: {a: 1}, update: {$inc: {a: 10}}})); - assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - const fad2 = assert.commandWorked(tokenDB.runCommand( - {findAndModify: kCollName, query: {a: 11}, update: {$set: {a: 1, b: 1}}})); - assert.eq({_id: 0, a: 11, b: 1}, fad2.value); - } + // Create a view on the collection, and check that listCollections sees the original + // collection, the view, and the system.views collection. Then, call $listCatalog on + // the collection and views and validate the results. + { + const viewName = "view1"; + const targetViews = 'system.views'; - // Create a view on the collection, and check that listCollections sees the original - // collection, the view, and the system.views collection. Then, call $listCatalog on - // the collection and views and validate the results. - { - const viewName = "view1"; - const targetViews = 'system.views'; - - assert.commandWorked( - tokenDB.runCommand({"create": viewName, "viewOn": kCollName, pipeline: []})); - - const colls = - assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); - assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); - const expectedColls = [ - {"name": kCollName, "type": "collection"}, - {"name": targetViews, "type": "collection"}, - {"name": viewName, "type": "view"} - ]; - assert(arrayEq(expectedColls, colls.cursor.firstBatch), - tojson(colls.cursor.firstBatch)); - checkNsSerializedCorrectly(featureFlagRequireTenantId, - kTenant, - kDbName, - "$cmd.listCollections", - colls.cursor.ns); - - const prefixedDbName = kTenant + '_' + tokenDB.getName(); - const targetDb = featureFlagRequireTenantId ? tokenDB.getName() : prefixedDbName; - - const tokenAdminDB = tokenConn.getDB('admin'); - - // Get catalog without specifying target collection (collectionless). - let result = - tokenAdminDB.runCommand({aggregate: 1, pipeline: [{$listCatalog: {}}], cursor: {}}); - let resultArray = result.cursor.firstBatch; - - // Check that the resulting array of catalog entries contains our target databases and - // namespaces. - assert( - resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); - - // Also check that the resulting array contains views specific to our target database. - assert(resultArray.some((entry) => - (entry.db === targetDb) && (entry.name === targetViews))); - assert( - resultArray.some((entry) => (entry.db === targetDb) && (entry.name === viewName))); - - // Get catalog when specifying our target collection, which should only return one - // result. - result = tokenDB.runCommand( - {aggregate: kCollName, pipeline: [{$listCatalog: {}}], cursor: {}}); - resultArray = result.cursor.firstBatch; - - // Check that the resulting array of catalog entries contains our target databases and - // namespaces. - assert(resultArray.length == 1); - assert( - resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); - } + assert.commandWorked( + tokenDB.runCommand({"create": viewName, "viewOn": kCollName, pipeline: []})); - // Test explain command with find - { - const cmdRes = tokenDB.runCommand({explain: {find: kCollName, filter: {a: 1}}}); - assert.eq(1, cmdRes.executionStats.nReturned, tojson(cmdRes)); - checkNsSerializedCorrectly(featureFlagRequireTenantId, - kTenant, - kDbName, - kCollName, - cmdRes.queryPlanner.namespace); - } + const colls = + assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); + assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); + const expectedColls = [ + {"name": kCollName, "type": "collection"}, + {"name": targetViews, "type": "collection"}, + {"name": viewName, "type": "view"} + ]; + assert(arrayEq(expectedColls, colls.cursor.firstBatch), tojson(colls.cursor.firstBatch)); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, "$cmd.listCollections", colls.cursor.ns); + + const prefixedDbName = kTenant + '_' + tokenDB.getName(); + const targetDb = featureFlagRequireTenantId ? tokenDB.getName() : prefixedDbName; + + const tokenAdminDB = tokenConn.getDB('admin'); + + // Get catalog without specifying target collection (collectionless). + let result = + tokenAdminDB.runCommand({aggregate: 1, pipeline: [{$listCatalog: {}}], cursor: {}}); + let resultArray = result.cursor.firstBatch; + + // Check that the resulting array of catalog entries contains our target databases and + // namespaces. + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); + + // Also check that the resulting array contains views specific to our target database. + assert( + resultArray.some((entry) => (entry.db === targetDb) && (entry.name === targetViews))); + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === viewName))); + + // Get catalog when specifying our target collection, which should only return one + // result. + result = + tokenDB.runCommand({aggregate: kCollName, pipeline: [{$listCatalog: {}}], cursor: {}}); + resultArray = result.cursor.firstBatch; + + // Check that the resulting array of catalog entries contains our target databases and + // namespaces. + assert(resultArray.length == 1); + assert(resultArray.some((entry) => (entry.db === targetDb) && (entry.name === kCollName))); + } - // Test count and distinct command. - { - assert.commandWorked(tokenDB.runCommand( - {insert: kCollName, documents: [{_id: 1, c: 1, d: 1}, {_id: 2, c: 1, d: 2}]})); + // Test explain command with find + { + const cmdRes = tokenDB.runCommand({explain: {find: kCollName, filter: {a: 1}}}); + assert.eq(1, cmdRes.executionStats.nReturned, tojson(cmdRes)); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, cmdRes.queryPlanner.namespace); + } - const resCount = - assert.commandWorked(tokenDB.runCommand({count: kCollName, query: {c: 1}})); - assert.eq(2, resCount.n); + // Test count and distinct command. + { + assert.commandWorked(tokenDB.runCommand( + {insert: kCollName, documents: [{_id: 1, c: 1, d: 1}, {_id: 2, c: 1, d: 2}]})); - const resDitinct = assert.commandWorked( - tokenDB.runCommand({distinct: kCollName, key: 'd', query: {}})); - assert.eq([1, 2], resDitinct.values.sort()); - } + const resCount = + assert.commandWorked(tokenDB.runCommand({count: kCollName, query: {c: 1}})); + assert.eq(2, resCount.n); - // Rename the collection. - { - const fromName = kDbName + '.' + kCollName; - const toName = fromName + "_renamed"; - const tokenAdminDB = tokenConn.getDB('admin'); - assert.commandWorked(tokenAdminDB.runCommand( - {renameCollection: fromName, to: toName, dropTarget: true})); - - // Verify the the renamed collection by findAndModify existing documents. - const fad1 = assert.commandWorked(tokenDB.runCommand({ - findAndModify: kCollName + "_renamed", - query: {a: 1}, - update: {$set: {a: 11, b: 1}} - })); - assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - - // Reset the collection name and document data. - assert.commandWorked(tokenAdminDB.runCommand( - {renameCollection: toName, to: fromName, dropTarget: true})); - assert.commandWorked(tokenDB.runCommand( - {findAndModify: kCollName, query: {a: 11}, update: {$set: {a: 1, b: 1}}})); - } + const resDitinct = + assert.commandWorked(tokenDB.runCommand({distinct: kCollName, key: 'd', query: {}})); + assert.eq([1, 2], resDitinct.values.sort()); + } - // ListDatabases only returns databases associated with kTenant. - { - // Create databases for kTenant. A new database is implicitly created when a collection - // is created. - const kOtherDbName = 'otherDb'; - assert.commandWorked(tokenConn.getDB(kOtherDbName).createCollection("collName")); - const tokenAdminDB = tokenConn.getDB('admin'); - const dbs = - assert.commandWorked(tokenAdminDB.runCommand({listDatabases: 1, nameOnly: true})); - assert.eq(3, dbs.databases.length); - const expectedDbs = featureFlagRequireTenantId - ? ["admin", kDbName, kOtherDbName] - : [kTenant + "_admin", kTenant + "_" + kDbName, kTenant + "_" + kOtherDbName]; - assert(arrayEq(expectedDbs, dbs.databases.map(db => db.name))); - } + // Rename the collection. + { + const fromName = kDbName + '.' + kCollName; + const toName = fromName + "_renamed"; + const tokenAdminDB = tokenConn.getDB('admin'); + assert.commandWorked( + tokenAdminDB.runCommand({renameCollection: fromName, to: toName, dropTarget: true})); - { - // Test the collStats command. - let res = assert.commandWorked(tokenDB.runCommand({collStats: kCollName})); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, res.ns); - - // perform the same test on a timeseries collection - const timeFieldName = "time"; - const tsColl = "timeseriesColl"; - assert.commandWorked( - tokenDB.createCollection(tsColl, {timeseries: {timeField: timeFieldName}})); - res = assert.commandWorked(tokenDB.runCommand({collStats: tsColl})); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, tsColl, res.ns); - checkNsSerializedCorrectly(featureFlagRequireTenantId, - kTenant, - kDbName, - 'system.buckets.' + tsColl, - res.timeseries.bucketsNs); - } + // Verify the the renamed collection by findAndModify existing documents. + const fad1 = assert.commandWorked(tokenDB.runCommand( + {findAndModify: kCollName + "_renamed", query: {a: 1}, update: {$set: {a: 11, b: 1}}})); + assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - // Drop the collection, and then the database. Check that listCollections no longer returns - // the 3 collections. - { - // Drop the collection, and check that the "ns" returned is serialized correctly. - const dropRes = assert.commandWorked(tokenDB.runCommand({drop: kCollName})); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, dropRes.ns); - - const collsAfterDropColl = assert.commandWorked(tokenDB.runCommand( - {listCollections: 1, nameOnly: true, filter: {name: kCollName}})); - assert.eq(0, - collsAfterDropColl.cursor.firstBatch.length, - tojson(collsAfterDropColl.cursor.firstBatch)); - - assert.commandWorked(tokenDB.runCommand({dropDatabase: 1})); - const collsAfterDropDb = - assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); - assert.eq(0, - collsAfterDropDb.cursor.firstBatch.length, - tojson(collsAfterDropDb.cursor.firstBatch)); - - // Reset the collection and document. - assert.commandWorked( - tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); - } + // Reset the collection name and document data. + assert.commandWorked( + tokenAdminDB.runCommand({renameCollection: toName, to: fromName, dropTarget: true})); + assert.commandWorked(tokenDB.runCommand( + {findAndModify: kCollName, query: {a: 11}, update: {$set: {a: 1, b: 1}}})); + } - // Test that transactions can be run successfully. - { - const session = tokenDB.getMongo().startSession(); - const sessionDb = session.getDatabase(kDbName); - session.startTransaction(); - assert.commandWorked(sessionDb.runCommand( - {delete: kCollName, deletes: [{q: {_id: 0, a: 1, b: 1}, limit: 1}]})); - session.commitTransaction_forTesting(); - - const findRes = assert.commandWorked(tokenDB.runCommand({find: kCollName})); - assert.eq(0, findRes.cursor.firstBatch.length, tojson(findRes.cursor.firstBatch)); - - // Reset the collection and document. - assert.commandWorked( - tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); - } + // ListDatabases only returns databases associated with kTenant. + { + // Create databases for kTenant. A new database is implicitly created when a collection + // is created. + const kOtherDbName = 'otherDb'; + assert.commandWorked(tokenConn.getDB(kOtherDbName).createCollection("collName")); + const tokenAdminDB = tokenConn.getDB('admin'); + const dbs = + assert.commandWorked(tokenAdminDB.runCommand({listDatabases: 1, nameOnly: true})); + assert.eq(3, dbs.databases.length); + const expectedDbs = featureFlagRequireTenantId + ? ["admin", kDbName, kOtherDbName] + : [kTenant + "_admin", kTenant + "_" + kDbName, kTenant + "_" + kOtherDbName]; + assert(arrayEq(expectedDbs, dbs.databases.map(db => db.name))); + } - // Test createIndexes, listIndexes and dropIndexes command. - { - var sortIndexesByName = function(indexes) { - return indexes.sort(function(a, b) { - return a.name > b.name; - }); - }; - - var getIndexesKeyAndName = function(indexes) { - return sortIndexesByName(indexes).map(function(index) { - return {key: index.key, name: index.name}; - }); - }; - - let res = assert.commandWorked(tokenDB.runCommand({ - createIndexes: kCollName, - indexes: [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}] - })); - assert.eq(3, res.numIndexesAfter); - - res = assert.commandWorked(tokenDB.runCommand({listIndexes: kCollName})); - assert.eq(3, res.cursor.firstBatch.length); - assert(arrayEq( - [ - {key: {"_id": 1}, name: "_id_"}, - {key: {a: 1}, name: "indexA"}, - {key: {b: 1}, name: "indexB"} - ], - getIndexesKeyAndName(res.cursor.firstBatch))); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, res.cursor.ns); - - // Drop those new created indexes. - res = assert.commandWorked( - tokenDB.runCommand({dropIndexes: kCollName, index: ["indexA", "indexB"]})); - - res = assert.commandWorked(tokenDB.runCommand({listIndexes: kCollName})); - assert.eq(1, res.cursor.firstBatch.length); - assert(arrayEq([{key: {"_id": 1}, name: "_id_"}], - getIndexesKeyAndName(res.cursor.firstBatch))); - } + { + // Test the collStats command. + let res = assert.commandWorked(tokenDB.runCommand({collStats: kCollName})); + checkNsSerializedCorrectly(featureFlagRequireTenantId, kTenant, kDbName, kCollName, res.ns); - // Test the validate command. - { - const validateRes = assert.commandWorked(tokenDB.runCommand({validate: kCollName})); - assert(validateRes.valid); - checkNsSerializedCorrectly( - featureFlagRequireTenantId, kTenant, kDbName, kCollName, validateRes.ns); - } + // perform the same test on a timeseries collection + const timeFieldName = "time"; + const tsColl = "timeseriesColl"; + assert.commandWorked( + tokenDB.createCollection(tsColl, {timeseries: {timeField: timeFieldName}})); + res = assert.commandWorked(tokenDB.runCommand({collStats: tsColl})); + checkNsSerializedCorrectly(featureFlagRequireTenantId, kTenant, kDbName, tsColl, res.ns); + checkNsSerializedCorrectly(featureFlagRequireTenantId, + kTenant, + kDbName, + 'system.buckets.' + tsColl, + res.timeseries.bucketsNs); } - // Test commands using a security token for a different tenant and check that this tenant cannot - // access the other tenant's collection. + // Drop the collection, and then the database. Check that listCollections no longer returns + // the 3 collections. { - // Create a user for a different tenant, and set the security token on the connection. - // We reuse the same connection, but swap the token out. - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant2", - '$tenant': kOtherTenant, - roles: [ - {role: 'dbAdminAnyDatabase', db: 'admin'}, - {role: 'readWriteAnyDatabase', db: 'admin'} - ] - })); - const securityTokenOtherTenant = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); - tokenConn._setSecurityToken(securityTokenOtherTenant); - - const tokenDB2 = tokenConn.getDB(kDbName); + // Drop the collection, and check that the "ns" returned is serialized correctly. + const dropRes = assert.commandWorked(tokenDB.runCommand({drop: kCollName})); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, dropRes.ns); + + const collsAfterDropColl = assert.commandWorked( + tokenDB.runCommand({listCollections: 1, nameOnly: true, filter: {name: kCollName}})); + assert.eq(0, + collsAfterDropColl.cursor.firstBatch.length, + tojson(collsAfterDropColl.cursor.firstBatch)); + + assert.commandWorked(tokenDB.runCommand({dropDatabase: 1})); + const collsAfterDropDb = + assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); + assert.eq(0, + collsAfterDropDb.cursor.firstBatch.length, + tojson(collsAfterDropDb.cursor.firstBatch)); - const findOtherUser = - assert.commandWorked(tokenDB2.runCommand({find: kCollName, filter: {a: 1}})); - assert.eq(findOtherUser.cursor.firstBatch.length, 0, tojson(findOtherUser)); + // Reset the collection and document. + assert.commandWorked( + tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); + } - const explainOtherUser = - assert.commandWorked(tokenDB2.runCommand({explain: {find: kCollName, filter: {a: 1}}})); - assert.eq(explainOtherUser.executionStats.nReturned, 0, tojson(explainOtherUser)); + // Test that transactions can be run successfully. + { + const session = tokenDB.getMongo().startSession(); + const sessionDb = session.getDatabase(kDbName); + session.startTransaction(); + assert.commandWorked(sessionDb.runCommand( + {delete: kCollName, deletes: [{q: {_id: 0, a: 1, b: 1}, limit: 1}]})); + session.commitTransaction_forTesting(); - const fadOtherUser = assert.commandWorked(tokenDB2.runCommand( - {findAndModify: kCollName, query: {b: 1}, update: {$inc: {b: 10}}})); - assert.eq(null, fadOtherUser.value); + const findRes = assert.commandWorked(tokenDB.runCommand({find: kCollName})); + assert.eq(0, findRes.cursor.firstBatch.length, tojson(findRes.cursor.firstBatch)); - const countOtherUser = - assert.commandWorked(tokenDB2.runCommand({count: kCollName, query: {c: 1}})); - assert.eq(0, countOtherUser.n); + // Reset the collection and document. + assert.commandWorked( + tokenDB.runCommand({insert: kCollName, documents: [{_id: 0, a: 1, b: 1}]})); + } - const distinctOtherUer = - assert.commandWorked(tokenDB2.runCommand({distinct: kCollName, key: 'd', query: {}})); - assert.eq([], distinctOtherUer.values); + // Test createIndexes, listIndexes and dropIndexes command. + { + var sortIndexesByName = function(indexes) { + return indexes.sort(function(a, b) { + return a.name > b.name; + }); + }; + + var getIndexesKeyAndName = function(indexes) { + return sortIndexesByName(indexes).map(function(index) { + return {key: index.key, name: index.name}; + }); + }; - const fromName = kDbName + '.' + kCollName; - const toName = fromName + "_renamed"; - assert.commandFailedWithCode( - tokenConn.getDB("admin").runCommand( - {renameCollection: fromName, to: toName, dropTarget: true}), - ErrorCodes.NamespaceNotFound); - - assert.commandFailedWithCode(tokenDB2.runCommand({listIndexes: kCollName}), - ErrorCodes.NamespaceNotFound); - assert.commandFailedWithCode( - tokenDB2.runCommand({dropIndexes: kCollName, index: ["indexA", "indexB"]}), - ErrorCodes.NamespaceNotFound); - - assert.commandFailedWithCode(tokenDB2.runCommand({validate: kCollName}), - ErrorCodes.NamespaceNotFound); - - // ListDatabases with securityToken of kOtherTenant cannot access databases created by - // kTenant. - const dbsWithDiffToken = assert.commandWorked( - tokenConn.getDB('admin').runCommand({listDatabases: 1, nameOnly: true})); - // Only the 'admin' db exists - assert.eq(1, dbsWithDiffToken.databases.length); - const expectedAdminDb = featureFlagRequireTenantId ? "admin" : kOtherTenant + "_admin"; - assert(arrayEq([expectedAdminDb], dbsWithDiffToken.databases.map(db => db.name))); - - // Attempt to drop the database, then check it was not dropped. - assert.commandWorked(tokenDB2.runCommand({dropDatabase: 1})); - - // Run listCollections using the original user's security token to see the collection - // exists. - tokenConn._setSecurityToken(securityToken); - const collsAfterDropOtherTenant = - assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); - assert.eq(1, - collsAfterDropOtherTenant.cursor.firstBatch.length, - tojson(collsAfterDropOtherTenant.cursor.firstBatch)); + let res = assert.commandWorked(tokenDB.runCommand({ + createIndexes: kCollName, + indexes: [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}] + })); + assert.eq(3, res.numIndexesAfter); + + res = assert.commandWorked(tokenDB.runCommand({listIndexes: kCollName})); + assert.eq(3, res.cursor.firstBatch.length); + assert(arrayEq( + [ + {key: {"_id": 1}, name: "_id_"}, + {key: {a: 1}, name: "indexA"}, + {key: {b: 1}, name: "indexB"} + ], + getIndexesKeyAndName(res.cursor.firstBatch))); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, res.cursor.ns); + + // Drop those new created indexes. + res = assert.commandWorked( + tokenDB.runCommand({dropIndexes: kCollName, index: ["indexA", "indexB"]})); + + res = assert.commandWorked(tokenDB.runCommand({listIndexes: kCollName})); + assert.eq(1, res.cursor.firstBatch.length); + assert(arrayEq([{key: {"_id": 1}, name: "_id_"}], + getIndexesKeyAndName(res.cursor.firstBatch))); } - // Test commands using a privleged user with ActionType::useTenant and check the user can still - // run commands on the doc when passing the correct tenant, but not when passing a different - // tenant. + // Test the validate command. { - const privelegedConn = new Mongo(primary.host); - assert(privelegedConn.getDB('admin').auth('admin', 'pwd')); - const privelegedDB = privelegedConn.getDB(kDbName); - - // Find and modify the document using $tenant. - { - const fadCorrectDollarTenant = assert.commandWorked(privelegedDB.runCommand({ - findAndModify: kCollName, - query: {b: 1}, - update: {$inc: {b: 10}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 1, b: 1}, fadCorrectDollarTenant.value); - - const fadOtherDollarTenant = assert.commandWorked(privelegedDB.runCommand({ - findAndModify: kCollName, - query: {b: 11}, - update: {$inc: {b: 10}}, - '$tenant': kOtherTenant - })); - assert.eq(null, fadOtherDollarTenant.value); - - // Reset document data. - assert.commandWorked(privelegedDB.runCommand({ - findAndModify: kCollName, - query: {b: 11}, - update: {$set: {a: 1, b: 1}}, - '$tenant': kTenant - })); - } - - // Rename the collection using $tenant. - { - const fromName = kDbName + '.' + kCollName; - const toName = fromName + "_renamed"; - const privelegedAdminDB = privelegedConn.getDB('admin'); - assert.commandWorked(privelegedAdminDB.runCommand( - {renameCollection: fromName, to: toName, dropTarget: true, '$tenant': kTenant})); - - // Verify the the renamed collection by findAndModify existing documents. - const fad1 = assert.commandWorked(privelegedDB.runCommand({ - findAndModify: kCollName + "_renamed", - query: {a: 1}, - update: {$set: {a: 11, b: 1}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 1, b: 1}, fad1.value); - - // Reset the collection name and document data. - assert.commandWorked(privelegedAdminDB.runCommand( - {renameCollection: toName, to: fromName, dropTarget: true, '$tenant': kTenant})); - assert.commandWorked(privelegedDB.runCommand({ - findAndModify: kCollName, - query: {a: 11}, - update: {$set: {a: 1, b: 1}}, - '$tenant': kTenant - })); - } + const validateRes = assert.commandWorked(tokenDB.runCommand({validate: kCollName})); + assert(validateRes.valid); + checkNsSerializedCorrectly( + featureFlagRequireTenantId, kTenant, kDbName, kCollName, validateRes.ns); } +} + +// Test commands using a security token for a different tenant and check that this tenant cannot +// access the other tenant's collection. +{ + // Create a user for a different tenant, and set the security token on the connection. + // We reuse the same connection, but swap the token out. + assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant2", + '$tenant': kOtherTenant, + roles: + [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] + })); + const securityTokenOtherTenant = + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); + tokenConn._setSecurityToken(securityTokenOtherTenant); + + const tokenDB2 = tokenConn.getDB(kDbName); + + const findOtherUser = + assert.commandWorked(tokenDB2.runCommand({find: kCollName, filter: {a: 1}})); + assert.eq(findOtherUser.cursor.firstBatch.length, 0, tojson(findOtherUser)); + + const explainOtherUser = + assert.commandWorked(tokenDB2.runCommand({explain: {find: kCollName, filter: {a: 1}}})); + assert.eq(explainOtherUser.executionStats.nReturned, 0, tojson(explainOtherUser)); + + const fadOtherUser = assert.commandWorked( + tokenDB2.runCommand({findAndModify: kCollName, query: {b: 1}, update: {$inc: {b: 10}}})); + assert.eq(null, fadOtherUser.value); + + const countOtherUser = + assert.commandWorked(tokenDB2.runCommand({count: kCollName, query: {c: 1}})); + assert.eq(0, countOtherUser.n); + + const distinctOtherUer = + assert.commandWorked(tokenDB2.runCommand({distinct: kCollName, key: 'd', query: {}})); + assert.eq([], distinctOtherUer.values); + + const fromName = kDbName + '.' + kCollName; + const toName = fromName + "_renamed"; + assert.commandFailedWithCode(tokenConn.getDB("admin").runCommand( + {renameCollection: fromName, to: toName, dropTarget: true}), + ErrorCodes.NamespaceNotFound); + + assert.commandFailedWithCode(tokenDB2.runCommand({listIndexes: kCollName}), + ErrorCodes.NamespaceNotFound); + assert.commandFailedWithCode( + tokenDB2.runCommand({dropIndexes: kCollName, index: ["indexA", "indexB"]}), + ErrorCodes.NamespaceNotFound); + + assert.commandFailedWithCode(tokenDB2.runCommand({validate: kCollName}), + ErrorCodes.NamespaceNotFound); + + // ListDatabases with securityToken of kOtherTenant cannot access databases created by + // kTenant. + const dbsWithDiffToken = assert.commandWorked( + tokenConn.getDB('admin').runCommand({listDatabases: 1, nameOnly: true})); + // Only the 'admin' db exists + assert.eq(1, dbsWithDiffToken.databases.length); + const expectedAdminDb = featureFlagRequireTenantId ? "admin" : kOtherTenant + "_admin"; + assert(arrayEq([expectedAdminDb], dbsWithDiffToken.databases.map(db => db.name))); + + // Attempt to drop the database, then check it was not dropped. + assert.commandWorked(tokenDB2.runCommand({dropDatabase: 1})); + + // Run listCollections using the original user's security token to see the collection + // exists. + tokenConn._setSecurityToken(securityToken); + const collsAfterDropOtherTenant = + assert.commandWorked(tokenDB.runCommand({listCollections: 1, nameOnly: true})); + assert.eq(1, + collsAfterDropOtherTenant.cursor.firstBatch.length, + tojson(collsAfterDropOtherTenant.cursor.firstBatch)); +} + +// Test commands using a privleged user with ActionType::useTenant and check the user can still +// run commands on the doc when passing the correct tenant, but not when passing a different +// tenant. +{ + const privelegedConn = new Mongo(primary.host); + assert(privelegedConn.getDB('admin').auth('admin', 'pwd')); + const privelegedDB = privelegedConn.getDB(kDbName); - // Test collMod + // Find and modify the document using $tenant. { - // Create the index used for collMod - let res = assert.commandWorked(tokenDB.runCommand({ - createIndexes: kCollName, - indexes: [{key: {c: 1}, name: "indexC", expireAfterSeconds: 50}] + const fadCorrectDollarTenant = assert.commandWorked(privelegedDB.runCommand({ + findAndModify: kCollName, + query: {b: 1}, + update: {$inc: {b: 10}}, + '$tenant': kTenant })); - assert.eq(2, res.numIndexesAfter); - jsTestLog(`Created index`); - - // Modify the index with the tenantId - res = assert.commandWorked(tokenDB.runCommand( - {"collMod": kCollName, "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}})); - assert.eq(50, res.expireAfterSeconds_old); - assert.eq(100, res.expireAfterSeconds_new); + assert.eq({_id: 0, a: 1, b: 1}, fadCorrectDollarTenant.value); - // Drop the index created - assert.commandWorked(tokenDB.runCommand({dropIndexes: kCollName, index: ["indexC"]})); + const fadOtherDollarTenant = assert.commandWorked(privelegedDB.runCommand({ + findAndModify: kCollName, + query: {b: 11}, + update: {$inc: {b: 10}}, + '$tenant': kOtherTenant + })); + assert.eq(null, fadOtherDollarTenant.value); + + // Reset document data. + assert.commandWorked(privelegedDB.runCommand({ + findAndModify: kCollName, + query: {b: 11}, + update: {$set: {a: 1, b: 1}}, + '$tenant': kTenant + })); } - // Test dbCheck command. - // This should fail since dbCheck is not supporting using a security token. + // Rename the collection using $tenant. { - assert.commandFailedWithCode(tokenDB.runCommand({dbCheck: kCollName}), - ErrorCodes.Unauthorized); + const fromName = kDbName + '.' + kCollName; + const toName = fromName + "_renamed"; + const privelegedAdminDB = privelegedConn.getDB('admin'); + assert.commandWorked(privelegedAdminDB.runCommand( + {renameCollection: fromName, to: toName, dropTarget: true, '$tenant': kTenant})); + + // Verify the the renamed collection by findAndModify existing documents. + const fad1 = assert.commandWorked(privelegedDB.runCommand({ + findAndModify: kCollName + "_renamed", + query: {a: 1}, + update: {$set: {a: 11, b: 1}}, + '$tenant': kTenant + })); + assert.eq({_id: 0, a: 1, b: 1}, fad1.value); + + // Reset the collection name and document data. + assert.commandWorked(privelegedAdminDB.runCommand( + {renameCollection: toName, to: fromName, dropTarget: true, '$tenant': kTenant})); + assert.commandWorked(privelegedDB.runCommand({ + findAndModify: kCollName, + query: {a: 11}, + update: {$set: {a: 1, b: 1}}, + '$tenant': kTenant + })); } +} - rst.stopSet(); +// Test collMod +{ + // Create the index used for collMod + let res = assert.commandWorked(tokenDB.runCommand({ + createIndexes: kCollName, + indexes: [{key: {c: 1}, name: "indexC", expireAfterSeconds: 50}] + })); + assert.eq(2, res.numIndexesAfter); + jsTestLog(`Created index`); + + // Modify the index with the tenantId + res = assert.commandWorked(tokenDB.runCommand( + {"collMod": kCollName, "index": {"keyPattern": {c: 1}, expireAfterSeconds: 100}})); + assert.eq(50, res.expireAfterSeconds_old); + assert.eq(100, res.expireAfterSeconds_new); + + // Drop the index created + assert.commandWorked(tokenDB.runCommand({dropIndexes: kCollName, index: ["indexC"]})); } -runTest(true); -runTest(false); + +// Test dbCheck command. +// This should fail since dbCheck is not supporting using a security token. +{ assert.commandFailedWithCode(tokenDB.runCommand({dbCheck: kCollName}), ErrorCodes.Unauthorized); } + +rst.stopSet(); })(); diff --git a/jstests/serverless/native_tenant_data_isolation_curr_op.js b/jstests/serverless/native_tenant_data_isolation_curr_op.js index 80d82ecf113..d4652e3bf4b 100644 --- a/jstests/serverless/native_tenant_data_isolation_curr_op.js +++ b/jstests/serverless/native_tenant_data_isolation_curr_op.js @@ -1,11 +1,12 @@ // Test that currentOp works as expected in a multitenant environment. -// @tags: [requires_fcv_62, featureFlagRequireTenantID] +// @tags: [requires_fcv_62] (function() { "use strict"; load('jstests/aggregation/extras/utils.js'); // For arrayEq() load("jstests/libs/fail_point_util.js"); // For configureFailPoint() load("jstests/libs/parallel_shell_helpers.js"); // For funWithArgs() +load("jstests/libs/feature_flag_util.js"); // for isEnabled const kTenant = ObjectId(); const kOtherTenant = ObjectId(); @@ -90,139 +91,124 @@ function checkNsSerializedCorrectly( } } -function runTest(featureFlagRequireTenantId) { - const rst = new ReplSetTest({ - nodes: 1, - nodeOptions: { - auth: '', - setParameter: { - multitenancySupport: true, - featureFlagSecurityToken: true, - featureFlagRequireTenantID: featureFlagRequireTenantId - } +const rst = new ReplSetTest({ + nodes: 1, + nodeOptions: { + auth: '', + setParameter: { + multitenancySupport: true, + featureFlagSecurityToken: true, } - }); - rst.startSet({keyFile: 'jstests/libs/key1'}); - rst.initiate(); - - const primary = rst.getPrimary(); - const adminDb = primary.getDB('admin'); - - // Must be authenticated as a user with ActionType::useTenant in order to use $tenant. - assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); - assert(adminDb.auth('admin', 'pwd')); - - // Create a non-privileged user for later use. - assert.commandWorked( - adminDb.runCommand({createUser: 'dbAdmin', pwd: 'pwd', roles: ['dbAdminAnyDatabase']})); - - const securityToken = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant1", - '$tenant': kTenant, - roles: - [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] - })); - - const securityTokenOtherTenant = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant2", - '$tenant': kOtherTenant, - roles: - [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] - })); - - // Set the security token for kTenant on the connection to start. - const tokenConn = new Mongo(primary.host); - tokenConn._setSecurityToken(securityToken); - - // Run an insert for kTenant that will implicitly create a collection, and force the collection - // creation to hang so that we'll capture it in the currentOp output. Turn on a failpoint to - // force the create to hang, and wait to hit the failpoint. - const createCollFP = configureFailPoint(primary, "hangBeforeLoggingCreateCollection"); - function runCreate(securityToken, dbName, kNewCollectionName) { - db.getMongo()._setSecurityToken(securityToken); - assert.commandWorked(db.getSiblingDB(dbName).runCommand( - {insert: kNewCollectionName, documents: [{_id: 0}]})); } - const createShell = startParallelShell( - funWithArgs(runCreate, securityToken, kDbName, kNewCollectionName), primary.port); - createCollFP.wait(); - - // Check that the 'insert' op shows up in the currOp output for 'kTenant' when issuing - // '$currentOp' in aggregation pipeline. - assertCurrentOpAggOutput(tokenConn, - adminDb, - kTenant, - kDbName, - 1 /* expectedBatchSize */, - featureFlagRequireTenantId); - - // Check that the 'insert' op shows up in the currOp output for 'kTenant' when issuing - // the currentOp command. - assertCurrentOpCommandOutput(tokenConn, - adminDb, - kTenant, - kDbName, - 1 /* expectedBatchSize */, - featureFlagRequireTenantId); - - // Check that the other tenant does not see the op in currentOp output. - tokenConn._setSecurityToken(securityTokenOtherTenant); - assertCurrentOpAggOutput(tokenConn, +}); +rst.startSet({keyFile: 'jstests/libs/key1'}); +rst.initiate(); + +const primary = rst.getPrimary(); +const adminDb = primary.getDB('admin'); + +// Must be authenticated as a user with ActionType::useTenant in order to use $tenant. +assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +assert(adminDb.auth('admin', 'pwd')); + +const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTenantID"); + +// Create a non-privileged user for later use. +assert.commandWorked( + adminDb.runCommand({createUser: 'dbAdmin', pwd: 'pwd', roles: ['dbAdminAnyDatabase']})); + +const securityToken = _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); +assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant1", + '$tenant': kTenant, + roles: [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] +})); + +const securityTokenOtherTenant = + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); +assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant2", + '$tenant': kOtherTenant, + roles: [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] +})); + +// Set the security token for kTenant on the connection to start. +const tokenConn = new Mongo(primary.host); +tokenConn._setSecurityToken(securityToken); + +// Run an insert for kTenant that will implicitly create a collection, and force the collection +// creation to hang so that we'll capture it in the currentOp output. Turn on a failpoint to +// force the create to hang, and wait to hit the failpoint. +const createCollFP = configureFailPoint(primary, "hangBeforeLoggingCreateCollection"); +function runCreate(securityToken, dbName, kNewCollectionName) { + db.getMongo()._setSecurityToken(securityToken); + assert.commandWorked( + db.getSiblingDB(dbName).runCommand({insert: kNewCollectionName, documents: [{_id: 0}]})); +} +const createShell = startParallelShell( + funWithArgs(runCreate, securityToken, kDbName, kNewCollectionName), primary.port); +createCollFP.wait(); + +// Check that the 'insert' op shows up in the currOp output for 'kTenant' when issuing +// '$currentOp' in aggregation pipeline. +assertCurrentOpAggOutput( + tokenConn, adminDb, kTenant, kDbName, 1 /* expectedBatchSize */, featureFlagRequireTenantId); + +// Check that the 'insert' op shows up in the currOp output for 'kTenant' when issuing +// the currentOp command. +assertCurrentOpCommandOutput( + tokenConn, adminDb, kTenant, kDbName, 1 /* expectedBatchSize */, featureFlagRequireTenantId); + +// Check that the other tenant does not see the op in currentOp output. +tokenConn._setSecurityToken(securityTokenOtherTenant); +assertCurrentOpAggOutput(tokenConn, + adminDb, + kOtherTenant, + kDbName, + 0 /* expectedBatchSize */, + featureFlagRequireTenantId); +assertCurrentOpCommandOutput(tokenConn, adminDb, kOtherTenant, kDbName, 0 /* expectedBatchSize */, featureFlagRequireTenantId); - assertCurrentOpCommandOutput(tokenConn, - adminDb, - kOtherTenant, - kDbName, - 0 /* expectedBatchSize */, - featureFlagRequireTenantId); - - // Now check that a privileged user can see this op using both $currentOp and the currentOp - // command when no tenantId is provided. - const currOpAggResPrivilegedUser = adminDb.runCommand({ - aggregate: 1, - pipeline: [{$currentOp: {allUsers: true}}, {$match: {op: "insert"}}], - cursor: {} - }); - assert.eq( - currOpAggResPrivilegedUser.cursor.firstBatch.length, 1, tojson(currOpAggResPrivilegedUser)); - - const currOpCmdResPrivilegedUser = - adminDb.runCommand({currentOp: 1, $ownOps: false, $all: true, op: "insert"}); - assert.eq(currOpCmdResPrivilegedUser.inprog.length, 1, tojson(currOpCmdResPrivilegedUser)); - - // Check that a user without required privileges, i.e. not associated with kTenant, is not - // authorized to issue '$currentOp' for other users. - const nonPrivilegedConn = new Mongo(primary.host); - assert(nonPrivilegedConn.getDB("admin").auth('dbAdmin', 'pwd')); - - assert.commandFailedWithCode(nonPrivilegedConn.getDB("admin").runCommand({ - aggregate: 1, - pipeline: [{$currentOp: {allUsers: true}}, {$match: {op: "insert"}}], - cursor: {} - }), - ErrorCodes.Unauthorized); - - // Check that a user without required privileges, i.e. not associated with kTenant, is not - // authorized to issue the currentOp command for other users. - assert.commandFailedWithCode(nonPrivilegedConn.getDB("admin").runCommand( - {currentOp: 1, $ownOps: false, $all: true, op: "insert"}), - ErrorCodes.Unauthorized); - - // Turn the failpoint off and wait for the create to finish. - createCollFP.off(); - createShell(); - - rst.stopSet(); -} -runTest(true); -runTest(false); +// Now check that a privileged user can see this op using both $currentOp and the currentOp +// command when no tenantId is provided. +const currOpAggResPrivilegedUser = adminDb.runCommand({ + aggregate: 1, + pipeline: [{$currentOp: {allUsers: true}}, {$match: {op: "insert"}}], + cursor: {} +}); +assert.eq( + currOpAggResPrivilegedUser.cursor.firstBatch.length, 1, tojson(currOpAggResPrivilegedUser)); + +const currOpCmdResPrivilegedUser = + adminDb.runCommand({currentOp: 1, $ownOps: false, $all: true, op: "insert"}); +assert.eq(currOpCmdResPrivilegedUser.inprog.length, 1, tojson(currOpCmdResPrivilegedUser)); + +// Check that a user without required privileges, i.e. not associated with kTenant, is not +// authorized to issue '$currentOp' for other users. +const nonPrivilegedConn = new Mongo(primary.host); +assert(nonPrivilegedConn.getDB("admin").auth('dbAdmin', 'pwd')); + +assert.commandFailedWithCode(nonPrivilegedConn.getDB("admin").runCommand({ + aggregate: 1, + pipeline: [{$currentOp: {allUsers: true}}, {$match: {op: "insert"}}], + cursor: {} +}), + ErrorCodes.Unauthorized); + +// Check that a user without required privileges, i.e. not associated with kTenant, is not +// authorized to issue the currentOp command for other users. +assert.commandFailedWithCode(nonPrivilegedConn.getDB("admin").runCommand( + {currentOp: 1, $ownOps: false, $all: true, op: "insert"}), + ErrorCodes.Unauthorized); + +// Turn the failpoint off and wait for the create to finish. +createCollFP.off(); +createShell(); + +rst.stopSet(); })(); diff --git a/jstests/serverless/native_tenant_data_isolation_initial_sync.js b/jstests/serverless/native_tenant_data_isolation_initial_sync.js index 08c35707814..678618facb4 100644 --- a/jstests/serverless/native_tenant_data_isolation_initial_sync.js +++ b/jstests/serverless/native_tenant_data_isolation_initial_sync.js @@ -6,131 +6,122 @@ "use strict"; load('jstests/aggregation/extras/utils.js'); // For arrayEq() +load("jstests/libs/feature_flag_util.js"); // for isEnabled -function runTest(featureFlagRequireTenantId) { - const rst = new ReplSetTest({ - nodes: 1, - nodeOptions: { - auth: '', - setParameter: { - multitenancySupport: true, - featureFlagSecurityToken: true, - featureFlagRequireTenantID: featureFlagRequireTenantId - } - } - }); - rst.startSet({keyFile: 'jstests/libs/key1'}); - rst.initiate(); - - const primary = rst.getPrimary(); - - const kTenant1 = ObjectId(); - const kTenant2 = ObjectId(); - const kDbName = "test"; - const kCollName = "foo"; - - const primaryConn = new Mongo(primary.host); - const primaryDB = primaryConn.getDB(kDbName); - - // Create users for two different tenants. - const adminDb = primary.getDB('admin'); - assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); - assert(adminDb.auth('admin', 'pwd')); - - const securityToken1 = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant1}); - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant1", - '$tenant': kTenant1, - roles: - [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] - })); - - const securityToken2 = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kTenant2}); - assert.commandWorked(primary.getDB('$external').runCommand({ - createUser: "userTenant2", - '$tenant': kTenant2, - roles: - [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] - })); - - // Logout the root user to avoid multiple authentication. - primaryConn.getDB("admin").logout(); - - // Create a collection, insert some data, and create indexes on the collection for tenant1. - primaryConn._setSecurityToken(securityToken1); - - const tenant1Docs = [{_id: 0, a: 1, b: 1}, {_id: 1, a: 2, b: 3}]; - assert.commandWorked(primaryDB.runCommand({insert: kCollName, documents: tenant1Docs})); - - const tenant1Idxs = [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}]; - let res = assert.commandWorked( - primaryDB.runCommand({createIndexes: kCollName, indexes: tenant1Idxs})); - assert.eq(3, res.numIndexesAfter); - - // Create a collections, insert some data, and create indexes on the collection for tenant2. - primaryConn._setSecurityToken(securityToken2); - - const tenant2Docs = [{_id: 10, a: 10, b: 10}, {_id: 11, a: 20, b: 30}]; - assert.commandWorked(primaryDB.runCommand({insert: kCollName, documents: tenant2Docs})); - - const tenant2Idxs = [{key: {a: -1}, name: "indexA"}, {key: {b: -1}, name: "indexB"}]; - res = assert.commandWorked( - primaryDB.runCommand({createIndexes: kCollName, indexes: tenant2Idxs})); - assert.eq(3, res.numIndexesAfter); - - // Add a new secondary to the replica set and wait for initial sync to finish. - const secondary = rst.add({ +const rst = new ReplSetTest({ + nodes: 1, + nodeOptions: { + auth: '', setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, - featureFlagRequireTenantID: featureFlagRequireTenantId } - }); - rst.reInitiate(); - rst.awaitReplication(); - rst.awaitSecondaryNodes(); - - // Check that we find the data for each tenant on the newly synced secondary. - const secondaryConn = new Mongo(secondary.host); - const secondaryDB = secondaryConn.getDB(kDbName); - secondaryConn.setSecondaryOk(); - - // Look for tenant1's data and indexes. - secondaryConn._setSecurityToken(securityToken1); - - const findTenant1Res = - assert.commandWorked(secondaryDB.runCommand({find: kCollName, filter: {}})); - assert(arrayEq(tenant1Docs, findTenant1Res.cursor.firstBatch), tojson(findTenant1Res)); - - res = assert.commandWorked(secondaryDB.runCommand({listIndexes: kCollName})); - assert.eq(3, res.cursor.firstBatch.length); - assert(arrayEq(tenant1Idxs.concat([ - {key: {"_id": 1}, name: "_id_"}, - ]), - res.cursor.firstBatch.map(function(index) { - return {key: index.key, name: index.name}; - }))); - - // Look for tenant2's data and indexes. - secondaryConn._setSecurityToken(securityToken2); - const findTenant2Res = - assert.commandWorked(secondaryDB.runCommand({find: kCollName, filter: {}})); - assert(arrayEq(tenant2Docs, findTenant2Res.cursor.firstBatch), tojson(findTenant2Res)); - - res = assert.commandWorked(secondaryDB.runCommand({listIndexes: kCollName})); - assert.eq(3, res.cursor.firstBatch.length); - assert(arrayEq(tenant2Idxs.concat([ - {key: {"_id": 1}, name: "_id_"}, - ]), - res.cursor.firstBatch.map(function(index) { - return {key: index.key, name: index.name}; - }))); - - rst.stopSet(); -} - -runTest(true); -runTest(false); + } +}); +rst.startSet({keyFile: 'jstests/libs/key1'}); +rst.initiate(); + +const primary = rst.getPrimary(); + +const kTenant1 = ObjectId(); +const kTenant2 = ObjectId(); +const kDbName = "test"; +const kCollName = "foo"; + +const primaryConn = new Mongo(primary.host); +const primaryDB = primaryConn.getDB(kDbName); + +// Create users for two different tenants. +const adminDb = primary.getDB('admin'); +assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +assert(adminDb.auth('admin', 'pwd')); + +const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTenantID"); + +const securityToken1 = + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant1}); +assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant1", + '$tenant': kTenant1, + roles: [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] +})); + +const securityToken2 = + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kTenant2}); +assert.commandWorked(primary.getDB('$external').runCommand({ + createUser: "userTenant2", + '$tenant': kTenant2, + roles: [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] +})); + +// Logout the root user to avoid multiple authentication. +primaryConn.getDB("admin").logout(); + +// Create a collection, insert some data, and create indexes on the collection for tenant1. +primaryConn._setSecurityToken(securityToken1); + +const tenant1Docs = [{_id: 0, a: 1, b: 1}, {_id: 1, a: 2, b: 3}]; +assert.commandWorked(primaryDB.runCommand({insert: kCollName, documents: tenant1Docs})); + +const tenant1Idxs = [{key: {a: 1}, name: "indexA"}, {key: {b: 1}, name: "indexB"}]; +let res = + assert.commandWorked(primaryDB.runCommand({createIndexes: kCollName, indexes: tenant1Idxs})); +assert.eq(3, res.numIndexesAfter); + +// Create a collections, insert some data, and create indexes on the collection for tenant2. +primaryConn._setSecurityToken(securityToken2); + +const tenant2Docs = [{_id: 10, a: 10, b: 10}, {_id: 11, a: 20, b: 30}]; +assert.commandWorked(primaryDB.runCommand({insert: kCollName, documents: tenant2Docs})); + +const tenant2Idxs = [{key: {a: -1}, name: "indexA"}, {key: {b: -1}, name: "indexB"}]; +res = assert.commandWorked(primaryDB.runCommand({createIndexes: kCollName, indexes: tenant2Idxs})); +assert.eq(3, res.numIndexesAfter); + +// Add a new secondary to the replica set and wait for initial sync to finish. +const secondary = rst.add({ + setParameter: { + multitenancySupport: true, + featureFlagSecurityToken: true, + } +}); +rst.reInitiate(); +rst.awaitReplication(); +rst.awaitSecondaryNodes(); + +// Check that we find the data for each tenant on the newly synced secondary. +const secondaryConn = new Mongo(secondary.host); +const secondaryDB = secondaryConn.getDB(kDbName); +secondaryConn.setSecondaryOk(); + +// Look for tenant1's data and indexes. +secondaryConn._setSecurityToken(securityToken1); + +const findTenant1Res = assert.commandWorked(secondaryDB.runCommand({find: kCollName, filter: {}})); +assert(arrayEq(tenant1Docs, findTenant1Res.cursor.firstBatch), tojson(findTenant1Res)); + +res = assert.commandWorked(secondaryDB.runCommand({listIndexes: kCollName})); +assert.eq(3, res.cursor.firstBatch.length); +assert(arrayEq(tenant1Idxs.concat([ + {key: {"_id": 1}, name: "_id_"}, +]), + res.cursor.firstBatch.map(function(index) { + return {key: index.key, name: index.name}; + }))); + +// Look for tenant2's data and indexes. +secondaryConn._setSecurityToken(securityToken2); +const findTenant2Res = assert.commandWorked(secondaryDB.runCommand({find: kCollName, filter: {}})); +assert(arrayEq(tenant2Docs, findTenant2Res.cursor.firstBatch), tojson(findTenant2Res)); + +res = assert.commandWorked(secondaryDB.runCommand({listIndexes: kCollName})); +assert.eq(3, res.cursor.firstBatch.length); +assert(arrayEq(tenant2Idxs.concat([ + {key: {"_id": 1}, name: "_id_"}, +]), + res.cursor.firstBatch.map(function(index) { + return {key: index.key, name: index.name}; + }))); + +rst.stopSet(); })(); diff --git a/jstests/serverless/native_tenant_data_isolation_stop_restart.js b/jstests/serverless/native_tenant_data_isolation_stop_restart.js index dd53b4d7f2c..10731785a4a 100644 --- a/jstests/serverless/native_tenant_data_isolation_stop_restart.js +++ b/jstests/serverless/native_tenant_data_isolation_stop_restart.js @@ -5,95 +5,80 @@ "use strict"; load('jstests/aggregation/extras/utils.js'); // For arrayEq() +load("jstests/libs/feature_flag_util.js"); // for isEnabled -function runTest(featureFlagRequireTenantId) { - const rst = new ReplSetTest({ - nodes: 3, - nodeOptions: { - auth: '', - setParameter: - {multitenancySupport: true, featureFlagRequireTenantID: featureFlagRequireTenantId} - } - }); - rst.startSet({keyFile: 'jstests/libs/key1'}); - rst.initiate(); - - let primary = rst.getPrimary(); - let adminDb = primary.getDB('admin'); - - // Must be authenticated as a user with ActionType::useTenant in order to use $tenant. - assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +const rst = + new ReplSetTest({nodes: 3, nodeOptions: {auth: '', setParameter: {multitenancySupport: true}}}); +rst.startSet({keyFile: 'jstests/libs/key1'}); +rst.initiate(); + +let primary = rst.getPrimary(); +let adminDb = primary.getDB('admin'); + +// Must be authenticated as a user with ActionType::useTenant in order to use $tenant. +assert.commandWorked(adminDb.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']})); +assert(adminDb.auth('admin', 'pwd')); + +const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTenantID"); + +{ + const kTenant = ObjectId(); + let testDb = primary.getDB('myDb0'); + + // Create a collection by inserting a document to it. + assert.commandWorked(testDb.runCommand( + {insert: 'myColl0', documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); + + // Run findAndModify on the document. + let fad = assert.commandWorked(testDb.runCommand( + {findAndModify: "myColl0", query: {a: 1}, update: {$inc: {a: 10}}, '$tenant': kTenant})); + assert.eq({_id: 0, a: 1, b: 1}, fad.value); + + // Create a view on the collection. + assert.commandWorked(testDb.runCommand( + {"create": "view1", "viewOn": "myColl0", pipeline: [], '$tenant': kTenant})); + + // Stop the rs and restart it. + rst.stopSet(null /* signal */, true /* forRestart */, {noCleanData: true}); + rst.startSet({restart: true}); + primary = rst.getPrimary(); + + adminDb = primary.getDB('admin'); assert(adminDb.auth('admin', 'pwd')); + testDb = primary.getDB('myDb0'); - { - const kTenant = ObjectId(); - let testDb = primary.getDB('myDb0'); - - // Create a collection by inserting a document to it. - assert.commandWorked(testDb.runCommand( - {insert: 'myColl0', documents: [{_id: 0, a: 1, b: 1}], '$tenant': kTenant})); - - // Run findAndModify on the document. - let fad = assert.commandWorked(testDb.runCommand({ - findAndModify: "myColl0", - query: {a: 1}, - update: {$inc: {a: 10}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 1, b: 1}, fad.value); - - // Create a view on the collection. - assert.commandWorked(testDb.runCommand( - {"create": "view1", "viewOn": "myColl0", pipeline: [], '$tenant': kTenant})); - - // Stop the rs and restart it. - rst.stopSet(null /* signal */, true /* forRestart */, {noCleanData: true}); - rst.startSet({restart: true}); - primary = rst.getPrimary(); - - adminDb = primary.getDB('admin'); - assert(adminDb.auth('admin', 'pwd')); - testDb = primary.getDB('myDb0'); - - // Assert we see 3 collections in the tenant's db 'myDb0' - the original collection we - // created, the view on it, and the system.views collection. - const colls = assert.commandWorked( - testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kTenant})); - assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); - const expectedColls = [ - {"name": "myColl0", "type": "collection"}, - {"name": "system.views", "type": "collection"}, - {"name": "view1", "type": "view"} - ]; - assert(arrayEq(expectedColls, colls.cursor.firstBatch), tojson(colls.cursor.firstBatch)); - - // Assert we can still run findAndModify on the doc. - fad = assert.commandWorked(testDb.runCommand({ - findAndModify: "myColl0", - query: {a: 11}, - update: {$inc: {a: 10}}, - '$tenant': kTenant - })); - assert.eq({_id: 0, a: 11, b: 1}, fad.value); - - const findAndModPrefixed = - primary.getDB(kTenant + '_myDb0') - .runCommand({findAndModify: "myColl0", query: {b: 1}, update: {$inc: {b: 10}}}); - if (!featureFlagRequireTenantId) { - // Check that we do find the doc when the tenantId was passed as a prefix, only if the - // feature flag is not enabled. In this case, the server still accepts prefixed names, - // and will parse the tenant from the db name. - assert.commandWorked(findAndModPrefixed); - assert.eq({_id: 0, a: 21, b: 1}, findAndModPrefixed.value); - } else { - // assert.commandFailed(findAndModPrefixed); - // TODO SERVER-70876 Uncomment out the check above, and remove the check below. - assert.eq(null, findAndModPrefixed.value); - } - } + // Assert we see 3 collections in the tenant's db 'myDb0' - the original collection we + // created, the view on it, and the system.views collection. + const colls = assert.commandWorked( + testDb.runCommand({listCollections: 1, nameOnly: true, '$tenant': kTenant})); + assert.eq(3, colls.cursor.firstBatch.length, tojson(colls.cursor.firstBatch)); + const expectedColls = [ + {"name": "myColl0", "type": "collection"}, + {"name": "system.views", "type": "collection"}, + {"name": "view1", "type": "view"} + ]; + assert(arrayEq(expectedColls, colls.cursor.firstBatch), tojson(colls.cursor.firstBatch)); - rst.stopSet(); + // Assert we can still run findAndModify on the doc. + fad = assert.commandWorked(testDb.runCommand( + {findAndModify: "myColl0", query: {a: 11}, update: {$inc: {a: 10}}, '$tenant': kTenant})); + assert.eq({_id: 0, a: 11, b: 1}, fad.value); + + const findAndModPrefixed = + primary.getDB(kTenant + '_myDb0') + .runCommand({findAndModify: "myColl0", query: {b: 1}, update: {$inc: {b: 10}}}); + if (!featureFlagRequireTenantId) { + // Check that we do find the doc when the tenantId was passed as a prefix, only if the + // feature flag is not enabled. In this case, the server still accepts prefixed names, + // and will parse the tenant from the db name. + assert.commandWorked(findAndModPrefixed); + assert.eq({_id: 0, a: 21, b: 1}, findAndModPrefixed.value); + } else { + // assert.commandFailed(findAndModPrefixed); + // TODO SERVER-70876 Uncomment out the check above, and remove the check below. + assert.eq(null, findAndModPrefixed.value); + } } -runTest(true); -runTest(false); + +rst.stopSet(); })(); |