From f8befd2f2256507faabba8316e8218bffc92adcf Mon Sep 17 00:00:00 2001 From: Sara Golemon Date: Wed, 23 Aug 2023 10:34:05 -0500 Subject: [PATCH] SERVER-80394 Improve securityToken handling --- .eslintrc.yml | 1 + ...with_security_token_jscore_passthrough.yml | 2 + jstests/auth/authz_tenant_access_control.js | 8 +- jstests/auth/security_token.js | 76 ++++--- jstests/auth/token_privileges.js | 17 +- .../override_methods/inject_security_token.js | 5 +- jstests/serverless/bench_test_with_tenant.js | 8 +- ...luster_parameter_op_observer_serverless.js | 3 +- .../serverless/libs/change_collection_util.js | 12 +- .../list_databases_for_all_tenants.js | 11 +- ...ant_data_isolation_basic_security_token.js | 7 +- .../native_tenant_data_isolation_curr_op.js | 9 +- ...ive_tenant_data_isolation_expect_prefix.js | 6 +- ...tive_tenant_data_isolation_initial_sync.js | 7 +- .../native_tenant_data_isolation_kill_op.js | 17 +- src/mongo/crypto/SConscript | 13 +- src/mongo/crypto/jws_validated_token.cpp | 44 ++-- src/mongo/crypto/jwt_types.idl | 5 + src/mongo/db/auth/SConscript | 5 +- src/mongo/db/auth/authorization_session.h | 4 + .../db/auth/authorization_session_impl.cpp | 13 +- .../db/auth/authorization_session_impl.h | 3 + .../db/auth/authorization_session_test.cpp | 49 +++-- src/mongo/db/auth/validated_tenancy_scope.cpp | 206 +++++++++++++++--- src/mongo/db/auth/validated_tenancy_scope.h | 58 +++-- ..._token.idl => validated_tenancy_scope.idl} | 22 +- .../db/auth/validated_tenancy_scope_test.cpp | 41 +++- src/mongo/db/basic_types.idl | 7 + src/mongo/embedded/embedded_auth_session.cpp | 4 + src/mongo/idl/SConscript | 1 + .../metadata/security_token_metadata_test.cpp | 51 +++-- src/mongo/rpc/op_msg.cpp | 15 +- src/mongo/rpc/op_msg.h | 6 +- src/mongo/rpc/op_msg_test.cpp | 18 +- src/mongo/scripting/mozjs/mongo.cpp | 9 +- src/mongo/shell/shell_utils.cpp | 32 ++- 36 files changed, 568 insertions(+), 227 deletions(-) rename src/mongo/db/auth/{security_token.idl => validated_tenancy_scope.idl} (72%) diff --git a/.eslintrc.yml b/.eslintrc.yml index d5f803949a1..cf46e6a0755 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -101,6 +101,7 @@ globals: isObject: true isString: true _createSecurityToken: true + _createTenantToken: true _isAddressSanitizerActive: true _isLeakSanitizerActive: true _isThreadSanitizerActive: true diff --git a/buildscripts/resmokeconfig/suites/native_tenant_data_isolation_with_security_token_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/native_tenant_data_isolation_with_security_token_jscore_passthrough.yml index 1f404e6915f..00646eba7ab 100644 --- a/buildscripts/resmokeconfig/suites/native_tenant_data_isolation_with_security_token_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/native_tenant_data_isolation_with_security_token_jscore_passthrough.yml @@ -88,6 +88,7 @@ executor: global_vars: TestData: &TestData hashTestNamesForMultitenancy: true + testOnlyValidatedTenancyScopeKey: secret hooks: # The CheckReplDBHash hook waits until all operations have replicated to and have been applied # on the secondaries, so we run the ValidateCollections hook after it to ensure we're @@ -105,6 +106,7 @@ executor: enableTestCommands: 1 multitenancySupport: true featureFlagSecurityToken: true + testOnlyValidatedTenancyScopeKey: secret # TODO SERVER-70547: remove featureFlagRequireTenantID from the parameters and have the # inject_security_token override to be able to test both tenant-prefixed request and non-tenant-prefixed request. # Currently, we only test non-tenant-prefixed request and enable the featureFlagRequireTenantID diff --git a/jstests/auth/authz_tenant_access_control.js b/jstests/auth/authz_tenant_access_control.js index 9405b54c720..8dd82e11d46 100644 --- a/jstests/auth/authz_tenant_access_control.js +++ b/jstests/auth/authz_tenant_access_control.js @@ -6,6 +6,7 @@ import {tenantCommand} from "jstests/libs/cluster_server_parameter_utils.js"; const tenantId1 = ObjectId(); const tenantId2 = ObjectId(); +const kVTSKey = 'secret'; // List of tests. Each test specifies a role which the created user should have, whether we // should be testing with a tenant ID, and the list of authorization checks to run and their @@ -222,7 +223,7 @@ function runTests(conn) { // users. if (test.isTenantedUser) { conn._setSecurityToken( - _createSecurityToken({user: username, db: 'admin', tenant: tenantId1})); + _createSecurityToken({user: username, db: 'admin', tenant: tenantId1}, kVTSKey)); } else { assert(admin.auth(username, 'pwd')); } @@ -260,7 +261,10 @@ function runTests(conn) { const opts = { auth: '', - setParameter: {multitenancySupport: true} + setParameter: { + multitenancySupport: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, + }, }; // Test on standalone and replset. diff --git a/jstests/auth/security_token.js b/jstests/auth/security_token.js index 6418d91d006..6bf511e425f 100644 --- a/jstests/auth/security_token.js +++ b/jstests/auth/security_token.js @@ -6,6 +6,7 @@ const kLogLevelForToken = 5; const kAcceptedSecurityTokenID = 5838100; const kLogoutMessageID = 6161506; const kStaleAuthenticationMessageID = 6161507; +const kVTSKey = 'secret'; const isSecurityTokenEnabled = TestData.setParameters.featureFlagSecurityToken; function assertNoTokensProcessedYet(conn) { @@ -14,19 +15,11 @@ function assertNoTokensProcessedYet(conn) { 'Unexpected security token has been processed'); } -function makeTokenAndExpect(user, db) { +function makeToken(user, db, secret = kVTSKey) { const authUser = {user: user, db: db, tenant: tenantID}; - - const token = _createSecurityToken(authUser); + const token = _createSecurityToken(authUser, secret); jsTest.log('Using security token: ' + tojson(token)); - - // Clone and rewrite OID and BinData fields to be roundtrip-safe. - const expect = Object.assign({}, token); - expect.authenticatedUser = Object.assign({}, token.authenticatedUser); - expect.authenticatedUser.tenant = {'$oid': tenantID.str}; - expect.sig = {'$binary': {base64: token.sig.base64(), subType: '0'}}; - - return [token, {token: expect}]; + return token; } function runTest(conn, multitenancyEnabled, rst = undefined) { @@ -36,11 +29,17 @@ function runTest(conn, multitenancyEnabled, rst = undefined) { // we are accessing system.users for another tenant. assert.commandWorked(admin.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['__system']})); assert(admin.auth('admin', 'pwd')); + // Make a less-privileged base user. + assert.commandWorked( + admin.runCommand({createUser: 'baseuser', pwd: 'pwd', roles: ['readWriteAnyDatabase']})); + + const baseConn = new Mongo(conn.host); + const baseAdmin = baseConn.getDB('admin'); + assert(baseAdmin.auth('baseuser', 'pwd')); // Create a tenant-local user. const createUserCmd = {createUser: 'user1', "$tenant": tenantID, pwd: 'pwd', roles: ['readWriteAnyDatabase']}; - const countUserCmd = {count: "system.users", query: {user: 'user1'}, "$tenant": tenantID}; if (multitenancyEnabled) { assert.commandWorked(admin.runCommand(createUserCmd)); @@ -49,8 +48,24 @@ function runTest(conn, multitenancyEnabled, rst = undefined) { assert.eq(admin.system.users.count({user: 'user1'}), 0, 'user1 should not exist on global users collection'); - const usersCount = assert.commandWorked(admin.runCommand(countUserCmd)); - assert.eq(usersCount.n, 1, 'user1 should exist on tenant users collection'); + + const countUserCmd = {count: "system.users", query: {user: 'user1'}}; + const countUserViaBody = Object.assign({}, countUserCmd, {"$tenant": tenantID}); + + // Count using {"$tenant":...} param in body. + const usersCountDollar = assert.commandWorked(admin.runCommand(countUserViaBody)); + assert.eq(usersCountDollar.n, 1, 'user1 should exist on tenant users collection'); + + // Count again using unsigned tenant token. + conn._setSecurityToken(_createTenantToken(tenantID)); + const usersCountToken = assert.commandWorked(admin.runCommand(countUserCmd)); + assert.eq(usersCountToken.n, 1, 'user1 should exist on tenant users collection'); + conn._setSecurityToken(undefined); + + // Users without `useTenant` should not be able to use unsigned tenant tokens. + baseConn._setSecurityToken(_createTenantToken(tenantID)); + assert.commandFailed(baseAdmin.runCommand(countUserCmd)); + baseConn._setSecurityToken(undefined); } else { assert.commandFailed(admin.runCommand(createUserCmd)); } @@ -67,24 +82,29 @@ function runTest(conn, multitenancyEnabled, rst = undefined) { const tokenDB = tokenConn.getDB('admin'); // Basic OP_MSG command. - tokenConn._setSecurityToken({}); + tokenConn._setSecurityToken(''); assert.commandWorked(tokenDB.runCommand({ping: 1})); assertNoTokensProcessedYet(conn); // Test that no token equates to unauthenticated. assert.commandFailed(tokenDB.runCommand({features: 1})); - // Passing a security token with unknown fields will fail at the client - // while trying to construct a signed security token. - const kIDLParserUnknownField = 40415; - tokenConn._setSecurityToken({invalid: 1}); - assert.throwsWithCode(() => tokenDB.runCommand({ping: 1}), kIDLParserUnknownField); - assertNoTokensProcessedYet(conn); - - const [token, expect] = makeTokenAndExpect('user1', 'admin'); - tokenConn._setSecurityToken(token); - if (multitenancyEnabled && isSecurityTokenEnabled) { + // Passing a security token with unknown fields will fail at the client + // while trying to construct a signed security token. + const kIDLMissingRequiredField = 40414; + tokenConn._setSecurityToken('e30.e30.deadbeefcafe'); // b64u('{}') === 'e30' + assert.commandFailedWithCode(tokenDB.runCommand({ping: 1}), kIDLMissingRequiredField); + assertNoTokensProcessedYet(conn); + + // Passing a valid looking security token signed with the wrong secret should also fail. + tokenConn._setSecurityToken(makeToken('user1', 'admin', 'haxx')); + assert.commandFailedWithCode(tokenDB.runCommand({ping: 1}), ErrorCodes.Unauthorized); + assertNoTokensProcessedYet(conn); + + const token = makeToken('user1', 'admin'); + tokenConn._setSecurityToken(token); + // Basic use. assert.commandWorked(tokenDB.runCommand({features: 1})); @@ -99,6 +119,7 @@ function runTest(conn, multitenancyEnabled, rst = undefined) { {role: 'readWriteAnyDatabase', db: 'admin'})); // Look for "Accepted Security Token" message with explicit tenant logging. + const expect = {token: token}; jsTest.log('Checking for: ' + tojson(expect)); checkLog.containsJson(conn, kAcceptedSecurityTokenID, expect, 'Security Token not logged'); @@ -135,7 +156,10 @@ function runTest(conn, multitenancyEnabled, rst = undefined) { function runTests(enabled) { const opts = { auth: '', - setParameter: "multitenancySupport=" + (enabled ? 'true' : 'false'), + setParameter: { + multitenancySupport: enabled, + testOnlyValidatedTenancyScopeKey: kVTSKey, + }, }; { const standalone = MongoRunner.runMongod(opts); diff --git a/jstests/auth/token_privileges.js b/jstests/auth/token_privileges.js index 6afedfdb662..350d43a754c 100644 --- a/jstests/auth/token_privileges.js +++ b/jstests/auth/token_privileges.js @@ -1,12 +1,8 @@ // Test role restrictions when using security tokens. -// @tags: [requires_replication] +// @tags: [requires_replication, featureFlagSecurityToken] const tenantID = ObjectId(); -const isSecurityTokenEnabled = TestData.setParameters.featureFlagSecurityToken; - -if (!isSecurityTokenEnabled) { - quit(); -} +const kVTSKey = 'secret'; function runTest(conn, rst = undefined) { const admin = conn.getDB('admin'); @@ -32,7 +28,7 @@ function runTest(conn, rst = undefined) { Object.keys(users).forEach(function(user) { const tokenConn = new Mongo(conn.host); tokenConn._setSecurityToken( - _createSecurityToken({user: user, db: '$external', tenant: tenantID})); + _createSecurityToken({user: user, db: '$external', tenant: tenantID}, kVTSKey)); const tokenDB = tokenConn.getDB('test'); if (users[user].prohibited) { assert.commandFailed(tokenDB.adminCommand({connectionStatus: 1})); @@ -58,7 +54,10 @@ function runTest(conn, rst = undefined) { const opts = { auth: '', - setParameter: "multitenancySupport=true", + setParameter: { + multitenancySupport: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, + }, }; { const standalone = MongoRunner.runMongod(opts); @@ -73,4 +72,4 @@ const opts = { rst.initiate(); runTest(rst.getPrimary(), rst); rst.stopSet(); -} \ No newline at end of file +} diff --git a/jstests/libs/override_methods/inject_security_token.js b/jstests/libs/override_methods/inject_security_token.js index 0a4d73b5503..ec798fbe3db 100644 --- a/jstests/libs/override_methods/inject_security_token.js +++ b/jstests/libs/override_methods/inject_security_token.js @@ -47,8 +47,11 @@ function prepareSecurityToken(conn) { if (typeof conn._securityToken == 'undefined') { print(`Inject security token to the connection: "${tojsononeline(conn)}", user: "${ kUserName}", tenant: ${kTenantId}`); + const key = TestData.testOnlyValidatedTenancyScopeKey; + assert.eq( + typeof key, 'string', 'testOnlyValidatedTenancyScopeKey not configured in TestData'); const securityToken = - _createSecurityToken({user: kUserName, db: '$external', tenant: kTenantId}); + _createSecurityToken({user: kUserName, db: '$external', tenant: kTenantId}, key); conn._setSecurityToken(securityToken); } } diff --git a/jstests/serverless/bench_test_with_tenant.js b/jstests/serverless/bench_test_with_tenant.js index d483aea10f2..25982d2ec65 100644 --- a/jstests/serverless/bench_test_with_tenant.js +++ b/jstests/serverless/bench_test_with_tenant.js @@ -6,12 +6,14 @@ * ] */ // Create a replica set with multi-tenancy enabled. +const kVTSKey = 'secret'; const replSetTest = ReplSetTest({nodes: 1}); replSetTest.startSet({ setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, - featureFlagRequireTenantID: true + featureFlagRequireTenantID: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } }); replSetTest.initiate(); @@ -39,7 +41,7 @@ const tenantDB = (() => { // Set the provided tenant id into the security token for the user. tokenConn._setSecurityToken( - _createSecurityToken({user: user, db: '$external', tenant: tenantId})); + _createSecurityToken({user: user, db: '$external', tenant: tenantId}, kVTSKey)); // Logout the root user to avoid multiple authentication. tokenConn.getDB("admin").logout(); @@ -194,4 +196,4 @@ benchRunSync({ response = tenantDB.id.getIndexes(); assert.eq(response.some(index => index.name === "newId_1"), false); -replSetTest.stopSet(); \ No newline at end of file +replSetTest.stopSet(); diff --git a/jstests/serverless/cluster_parameter_op_observer_serverless.js b/jstests/serverless/cluster_parameter_op_observer_serverless.js index eb082c1808f..095920a0293 100644 --- a/jstests/serverless/cluster_parameter_op_observer_serverless.js +++ b/jstests/serverless/cluster_parameter_op_observer_serverless.js @@ -37,7 +37,8 @@ rst.startSet({ setParameter: { multitenancySupport: true, featureFlagRequireTenantID: true, - featureFlagSecurityToken: true + featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: ChangeStreamMultitenantReplicaSetTest.getTokenKey(), } }); rst.initiate(); diff --git a/jstests/serverless/libs/change_collection_util.js b/jstests/serverless/libs/change_collection_util.js index 857d2f1bbe1..f6513bf0b09 100644 --- a/jstests/serverless/libs/change_collection_util.js +++ b/jstests/serverless/libs/change_collection_util.js @@ -78,7 +78,8 @@ export class ChangeStreamMultitenantReplicaSetTest extends ReplSetTest { featureFlagServerlessChangeStreams: true, multitenancySupport: true, featureFlagSecurityToken: true, - featureFlagRequireTenantID: true + featureFlagRequireTenantID: true, + testOnlyValidatedTenancyScopeKey: ChangeStreamMultitenantReplicaSetTest.getTokenKey(), }; const nodeOptions = config.nodeOptions || {}; @@ -113,6 +114,11 @@ export class ChangeStreamMultitenantReplicaSetTest extends ReplSetTest { }; } + // Exposed as a method because linter does not yet support static properties. + static getTokenKey() { + return "secret"; + } + // Returns a connection to the 'hostAddr' with 'tenantId' stamped to it for the created user. static getTenantConnection(hostAddr, tenantId, @@ -143,8 +149,10 @@ export class ChangeStreamMultitenantReplicaSetTest extends ReplSetTest { } // Set the provided tenant id into the security token for the user. + // PSK for signature matches testOnlyValidatedTenancyScopeKey setting in fixture class. tokenConn._setSecurityToken( - _createSecurityToken({user: user, db: '$external', tenant: tenantId})); + _createSecurityToken({user: user, db: '$external', tenant: tenantId}, + ChangeStreamMultitenantReplicaSetTest.getTokenKey())); // Logout the root user to avoid multiple authentication. tokenConn.getDB("admin").logout(); diff --git a/jstests/serverless/list_databases_for_all_tenants.js b/jstests/serverless/list_databases_for_all_tenants.js index 8debd021fd7..5f5eca3b46b 100644 --- a/jstests/serverless/list_databases_for_all_tenants.js +++ b/jstests/serverless/list_databases_for_all_tenants.js @@ -3,6 +3,8 @@ */ import {arrayEq} from "jstests/aggregation/extras/utils.js"; +const kVTSKey = 'secret'; + // Given the output from the listDatabasesForAllTenants command, ensures that the total size // reported is the sum of the individual db sizes. function verifySizeSum(listDatabasesOut) { @@ -49,7 +51,8 @@ function createMultitenantDatabases(conn, tokenConn, num) { roles: [{role: 'readWriteAnyDatabase', db: 'admin'}] })); tokenConn._setSecurityToken(_createSecurityToken( - {user: "readWriteUserTenant" + i.toString(), db: '$external', tenant: kTenant})); + {user: "readWriteUserTenant" + i.toString(), db: '$external', tenant: kTenant}, + kVTSKey)); // Create a collection for the tenant and then insert into it. const tokenDB = tokenConn.getDB('auto_gen_db_' + i.toString()); @@ -218,7 +221,7 @@ function runTestInvalidCommands(primary) { roles: [{role: 'readWriteAnyDatabase', db: 'admin'}] })); tokenConn._setSecurityToken( - _createSecurityToken({user: "unauthorizedUsr", db: '$external', tenant: kTenant})); + _createSecurityToken({user: "unauthorizedUsr", db: '$external', tenant: kTenant}, kVTSKey)); const tokenAdminDB = tokenConn.getDB("admin"); cmdRes = assert.commandFailedWithCode( tokenAdminDB.runCommand({listDatabasesForAllTenants: 1, filter: {name: /auto_gen_db_/}}), @@ -233,6 +236,7 @@ function runTestsWithMultiTenancySupport() { setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -263,6 +267,7 @@ function runTestNoMultiTenancySupport() { setParameter: { multitenancySupport: false, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -283,4 +288,4 @@ function runTestNoMultiTenancySupport() { } runTestsWithMultiTenancySupport(); -runTestNoMultiTenancySupport(); \ No newline at end of file +runTestNoMultiTenancySupport(); 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 5e1c927ef4d..c88ea2d2075 100644 --- a/jstests/serverless/native_tenant_data_isolation_basic_security_token.js +++ b/jstests/serverless/native_tenant_data_isolation_basic_security_token.js @@ -8,6 +8,7 @@ function checkNsSerializedCorrectly(kDbName, kCollectionName, nsField) { assert.eq(nsField, nss); } +const kVTSKey = 'secret'; const rst = new ReplSetTest({ nodes: 2, nodeOptions: { @@ -15,6 +16,7 @@ const rst = new ReplSetTest({ setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -40,7 +42,8 @@ const kDbName = 'myDb'; const kCollName = 'myColl'; const kViewName = "view1"; const tokenConn = new Mongo(primary.host); -const securityToken = _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); +const securityToken = + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}, kVTSKey); const tokenDB = tokenConn.getDB(kDbName); // In this jstest, the collection (defined by kCollName) and the document "{_id: 0, a: 1, b: 1}" @@ -511,7 +514,7 @@ const tokenDB = tokenConn.getDB(kDbName); [{role: 'dbAdminAnyDatabase', db: 'admin'}, {role: 'readWriteAnyDatabase', db: 'admin'}] })); const securityTokenOtherTenant = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}, kVTSKey); tokenConn._setSecurityToken(securityTokenOtherTenant); const tokenDB2 = tokenConn.getDB(kDbName); diff --git a/jstests/serverless/native_tenant_data_isolation_curr_op.js b/jstests/serverless/native_tenant_data_isolation_curr_op.js index 80c7c12436a..693e5a005bd 100644 --- a/jstests/serverless/native_tenant_data_isolation_curr_op.js +++ b/jstests/serverless/native_tenant_data_isolation_curr_op.js @@ -8,6 +8,7 @@ const kTenant = ObjectId(); const kOtherTenant = ObjectId(); const kDbName = 'myDb'; const kNewCollectionName = "currOpColl"; +const kVTSKey = 'secret'; // Check for the 'insert' op(s) in the currOp output for 'tenantId' when issuing '$currentOp' in // aggregation pipeline with a security token. @@ -77,6 +78,7 @@ const rst = new ReplSetTest({ setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -96,7 +98,8 @@ const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTe assert.commandWorked( adminDb.runCommand({createUser: 'dbAdmin', pwd: 'pwd', roles: ['dbAdminAnyDatabase']})); -const securityToken = _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); +const securityToken = + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant1", '$tenant': kTenant, @@ -104,7 +107,7 @@ assert.commandWorked(primary.getDB('$external').runCommand({ })); const securityTokenOtherTenant = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant2", '$tenant': kOtherTenant, @@ -236,4 +239,4 @@ tokenConn._setSecurityToken(securityToken); createShell(); } -rst.stopSet(); \ No newline at end of file +rst.stopSet(); diff --git a/jstests/serverless/native_tenant_data_isolation_expect_prefix.js b/jstests/serverless/native_tenant_data_isolation_expect_prefix.js index 05e01933fd3..f8f1e934a0a 100644 --- a/jstests/serverless/native_tenant_data_isolation_expect_prefix.js +++ b/jstests/serverless/native_tenant_data_isolation_expect_prefix.js @@ -7,7 +7,8 @@ import {arrayEq} from "jstests/aggregation/extras/utils.js"; const kTenant = ObjectId(); const kTestDb = 'testDb0'; const kCollName = 'myColl0'; -const kViewName = `myView0`; +const kViewName = 'myView0'; +const kVTSKey = 'secret'; function checkNsSerializedCorrectly(kDbName, kCollectionName, nsField, options) { options = options || {}; @@ -91,6 +92,7 @@ function runTestWithSecurityTokenFlag() { setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -107,7 +109,7 @@ function runTestWithSecurityTokenFlag() { const tokenConn = new Mongo(primary.host); const securityToken = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}, kVTSKey); const tokenDb = tokenConn.getDB(kTestDb); const prefixedTokenDb = tokenConn.getDB(kTenant + '_' + kTestDb); diff --git a/jstests/serverless/native_tenant_data_isolation_initial_sync.js b/jstests/serverless/native_tenant_data_isolation_initial_sync.js index fcbddd1cfd4..dede4398b55 100644 --- a/jstests/serverless/native_tenant_data_isolation_initial_sync.js +++ b/jstests/serverless/native_tenant_data_isolation_initial_sync.js @@ -5,6 +5,7 @@ import {arrayEq} from "jstests/aggregation/extras/utils.js"; import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js"; +const kVTSKey = 'secret'; const rst = new ReplSetTest({ nodes: 1, nodeOptions: { @@ -12,6 +13,7 @@ const rst = new ReplSetTest({ setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } } }); @@ -36,7 +38,7 @@ assert(adminDb.auth('admin', 'pwd')); const featureFlagRequireTenantId = FeatureFlagUtil.isEnabled(adminDb, "RequireTenantID"); const securityToken1 = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant1}); + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant1}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant1", '$tenant': kTenant1, @@ -44,7 +46,7 @@ assert.commandWorked(primary.getDB('$external').runCommand({ })); const securityToken2 = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kTenant2}); + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kTenant2}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant2", '$tenant': kTenant2, @@ -80,6 +82,7 @@ const secondary = rst.add({ setParameter: { multitenancySupport: true, featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey, } }); rst.reInitiate(); diff --git a/jstests/serverless/native_tenant_data_isolation_kill_op.js b/jstests/serverless/native_tenant_data_isolation_kill_op.js index 8d755fdbaa8..1af62a66987 100644 --- a/jstests/serverless/native_tenant_data_isolation_kill_op.js +++ b/jstests/serverless/native_tenant_data_isolation_kill_op.js @@ -11,10 +11,17 @@ function killCurrentOpTest() { assert.commandWorked(db.getSiblingDB(dbName).runCommand(insertCmdObj)); } + const kVTSKey = 'secret'; const rst = new ReplSetTest({ nodes: 3, - nodeOptions: - {auth: '', setParameter: {multitenancySupport: true, featureFlagSecurityToken: true}} + nodeOptions: { + auth: '', + setParameter: { + multitenancySupport: true, + featureFlagSecurityToken: true, + testOnlyValidatedTenancyScopeKey: kVTSKey + } + } }); rst.startSet({keyFile: 'jstests/libs/key1'}); rst.initiate(); @@ -34,7 +41,7 @@ function killCurrentOpTest() { // Create a user for kTenant and its security token. const securityToken = - _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}); + _createSecurityToken({user: "userTenant1", db: '$external', tenant: kTenant}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant1", '$tenant': kTenant, @@ -44,7 +51,7 @@ function killCurrentOpTest() { // Create a different tenant to test that one tenant can't see or kill other tenant's op. const securityTokenOtherTenant = - _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}); + _createSecurityToken({user: "userTenant2", db: '$external', tenant: kOtherTenant}, kVTSKey); assert.commandWorked(primary.getDB('$external').runCommand({ createUser: "userTenant2", '$tenant': kOtherTenant, @@ -99,4 +106,4 @@ function killCurrentOpTest() { rst.stopSet(); } -killCurrentOpTest(); \ No newline at end of file +killCurrentOpTest(); diff --git a/src/mongo/crypto/SConscript b/src/mongo/crypto/SConscript index 95984ed0302..e3542a02804 100644 --- a/src/mongo/crypto/SConscript +++ b/src/mongo/crypto/SConscript @@ -172,23 +172,32 @@ env.CppUnitTest( ], ) +env.Library( + target='jwt_types', + source='jwt_types.idl', + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/server_base', + '$BUILD_DIR/mongo/idl/idl_parser', + ], +) + env.Library( target='jwt', source=[ 'jwks_fetcher_impl.cpp', 'jwk_manager.cpp', - 'jwt_types.idl', 'jws_validated_token.cpp', 'jws_validator_{}.cpp'.format(ssl_provider), ], LIBDEPS=[ '$BUILD_DIR/mongo/base', + 'jwt_types', ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/client/sasl_client', '$BUILD_DIR/mongo/db/auth/oidc_protocol', '$BUILD_DIR/mongo/db/commands/test_commands_enabled', - '$BUILD_DIR/mongo/idl/idl_parser', '$BUILD_DIR/mongo/util/net/http_client_impl', ], ) diff --git a/src/mongo/crypto/jws_validated_token.cpp b/src/mongo/crypto/jws_validated_token.cpp index bf4b6302040..bbdc913d15f 100644 --- a/src/mongo/crypto/jws_validated_token.cpp +++ b/src/mongo/crypto/jws_validated_token.cpp @@ -49,31 +49,35 @@ namespace mongo::crypto { namespace { -struct ParsedToken { - StringData token[3]; +struct ParsedTokenView { + StringData header; + StringData body; + StringData signature; + StringData payload; }; // Split "header.body.signature" into {"header", "body", "signature", "header.body"} -ParsedToken parseSignedToken(StringData token) { - ParsedToken pt; - std::size_t split, pos = 0; - for (int i = 0; i < 3; ++i) { - split = token.find('.', pos); - pt.token[i] = token.substr(pos, split - pos); - pos = split + 1; +ParsedTokenView parseSignedToken(StringData token) { + ParsedTokenView pt; - if (i == 1) { - // Payload: encoded header + '.' + encoded body - pt.payload = token.substr(0, split); - } - } + auto split = token.find('.', 0); + uassert(8039401, "Missing JWS delimiter", split != std::string::npos); + pt.header = token.substr(0, split); + auto pos = split + 1; - uassert(7095400, "Unknown format of token", split == std::string::npos); + split = token.find('.', pos); + uassert(8039402, "Missing JWS delimiter", split != std::string::npos); + pt.body = token.substr(pos, split - pos); + pt.payload = token.substr(0, split); + pos = split + 1; + + split = token.find('.', pos); + uassert(8039403, "Too many delimiters in JWS token", split == std::string::npos); + pt.signature = token.substr(pos); return pt; } } // namespace - Status JWSValidatedToken::validate(JWKManager* keyMgr) const { const auto now = Date_t::now(); @@ -93,7 +97,7 @@ Status JWSValidatedToken::validate(JWKManager* keyMgr) const { } auto tokenSplit = parseSignedToken(_originalToken); - auto signature = base64url::decode(tokenSplit.token[2]); + auto signature = base64url::decode(tokenSplit.signature); auto payload = tokenSplit.payload; auto swValidator = keyMgr->getValidator(_header.getKeyId()); @@ -113,12 +117,12 @@ JWSValidatedToken::JWSValidatedToken(JWKManager* keyMgr, StringData token) : _originalToken(token.toString()) { auto tokenSplit = parseSignedToken(token); - auto headerString = base64url::decode(tokenSplit.token[0]); + auto headerString = base64url::decode(tokenSplit.header); _headerBSON = fromjson(headerString); _header = JWSHeader::parse(IDLParserContext("JWSHeader"), _headerBSON); uassert(7095401, "Unknown type of token", !_header.getType() || _header.getType() == "JWT"_sd); - auto bodyString = base64url::decode(tokenSplit.token[1]); + auto bodyString = base64url::decode(tokenSplit.body); _bodyBSON = fromjson(bodyString); _body = JWT::parse(IDLParserContext("JWT"), _bodyBSON); @@ -128,7 +132,7 @@ JWSValidatedToken::JWSValidatedToken(JWKManager* keyMgr, StringData token) StatusWith JWSValidatedToken::extractIssuerFromCompactSerialization( StringData token) try { auto tokenSplit = parseSignedToken(token); - auto payload = fromjson(base64url::decode(tokenSplit.token[1])); + auto payload = fromjson(base64url::decode(tokenSplit.body)); return JWT::parse(IDLParserContext{"JWT"}, payload).getIssuer().toString(); } catch (const DBException& ex) { return ex.toStatus(); diff --git a/src/mongo/crypto/jwt_types.idl b/src/mongo/crypto/jwt_types.idl index ed23a57ae47..8f60f6bc37e 100644 --- a/src/mongo/crypto/jwt_types.idl +++ b/src/mongo/crypto/jwt_types.idl @@ -111,6 +111,11 @@ structs: type: variant: [string, array] cpp_name: audience + "mongodb/tenantId": + description: TenantId; 24 hexits identifying the tenant for this operation + type: tenant_id_hex + cpp_name: tenantId + optional: true nbf: description: Time at which the JWT becomes valid. (Unix Epoch) type: unixEpoch diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript index d826887fc6b..00b39e2d9c0 100644 --- a/src/mongo/db/auth/SConscript +++ b/src/mongo/db/auth/SConscript @@ -8,9 +8,12 @@ env.Library( target='security_token', source=[ 'security_token_authentication_guard.cpp', - 'security_token.idl', + 'validated_tenancy_scope.idl', 'validated_tenancy_scope.cpp', ], + LIBDEPS=[ + '$BUILD_DIR/mongo/crypto/jwt_types', + ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/auth/auth', '$BUILD_DIR/mongo/db/server_base', diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h index 775a922840c..f1bc44bc098 100644 --- a/src/mongo/db/auth/authorization_session.h +++ b/src/mongo/db/auth/authorization_session.h @@ -335,6 +335,10 @@ public: // isAuthenticated() is also expected to return false. virtual bool isExpired() const = 0; + // When the current authorization will expire. + // boost::none indicates a non-expiring session. + virtual const boost::optional& getExpiration() const = 0; + protected: virtual std::tuple*, std::vector*> _getImpersonations() = 0; }; diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp index 519c1f043a5..5449744b13e 100644 --- a/src/mongo/db/auth/authorization_session_impl.cpp +++ b/src/mongo/db/auth/authorization_session_impl.cpp @@ -303,9 +303,12 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, uassert(6161502, "Attempt to authorize a user other than that present in the security token", validatedTenancyScope->authenticatedUser() == userName); - uassert(7070101, - "Attempt to set expiration policy on a security token user", - expirationTime == boost::none); + auto tokenExpires = validatedTenancyScope->getExpiration(); + if (!expirationTime) { + expirationTime = tokenExpires; + } else if (tokenExpires < expirationTime.get()) { + expirationTime = tokenExpires; + } validateSecurityTokenUserPrivileges(user->getPrivileges()); _authenticationMode = AuthenticationMode::kSecurityToken; } else { @@ -315,10 +318,10 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, expirationTime.value() > opCtx->getServiceContext()->getFastClockSource()->now()); _authenticationMode = AuthenticationMode::kConnection; - _expirationTime = std::move(expirationTime); - _expiredUserName = boost::none; } _authenticatedUser = std::move(user); + _expirationTime = std::move(expirationTime); + _expiredUserName = boost::none; // If there are any users and roles in the impersonation data, clear it out. clearImpersonatedUserData(); diff --git a/src/mongo/db/auth/authorization_session_impl.h b/src/mongo/db/auth/authorization_session_impl.h index 548920fb453..9b2200669a0 100644 --- a/src/mongo/db/auth/authorization_session_impl.h +++ b/src/mongo/db/auth/authorization_session_impl.h @@ -179,6 +179,9 @@ public: bool mayBypassWriteBlockingMode() const override; bool isExpired() const override; + const boost::optional& getExpiration() const override { + return _expirationTime; + } protected: friend class AuthorizationSessionImplTestHelper; diff --git a/src/mongo/db/auth/authorization_session_test.cpp b/src/mongo/db/auth/authorization_session_test.cpp index bf09708b4c7..05067fe70cb 100644 --- a/src/mongo/db/auth/authorization_session_test.cpp +++ b/src/mongo/db/auth/authorization_session_test.cpp @@ -68,7 +68,6 @@ #include "mongo/db/auth/restriction_environment.h" #include "mongo/db/auth/role_name.h" #include "mongo/db/auth/sasl_options.h" -#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/user.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/auth/validated_tenancy_scope.h" @@ -1604,29 +1603,48 @@ TEST_F(AuthorizationSessionTest, ExpiredSessionWithReauth) { TEST_F(AuthorizationSessionTest, ExpirationWithSecurityTokenNOK) { + constexpr auto kVTSKey = "secret"_sd; RAIIServerParameterControllerForTest multitenanyController("multitenancySupport", true); + RAIIServerParameterControllerForTest secretController("testOnlyValidatedTenancyScopeKey", + kVTSKey); // Tests authorization flow from unauthenticated to active (via token) to unauthenticated to // active (via stateful connection) to unauthenticated. using VTS = auth::ValidatedTenancyScope; // Create and authorize a security token user. - constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName; - ASSERT_OK(createUser(kTenant1UserTest, {{"readWrite", "test"}, {"dbAdmin", "test"}})); ASSERT_OK(createUser(kUser1Test, {{"readWriteAnyDatabase", "admin"}})); ASSERT_OK(createUser(kTenant2UserTest, {{"readWriteAnyDatabase", "admin"}})); { - VTS validatedTenancyScope = - VTS(BSON(authUserFieldName << kTenant1UserTest.toBSON(true /* encodeTenant */)), - VTS::TokenForTestingTag{}); - VTS::set(_opCtx.get(), validatedTenancyScope); + VTS validatedTenancyScope(kTenant1UserTest, kVTSKey, VTS::TokenForTestingTag{}); - // Make sure that security token users can't be authorized with an expiration date. - Date_t expirationTime = clockSource()->now() + Hours(1); - ASSERT_NOT_OK(authzSession->addAndAuthorizeUser( - _opCtx.get(), kTenant1UserTestRequest, expirationTime)); + // Actual expiration used by AuthorizationSession will be the minimum of + // the token's known expiraiton time and the expiration time passed in. + const auto checkExpiration = [&](const boost::optional& expire, + const Date_t& expect) { + VTS::set(_opCtx.get(), validatedTenancyScope); + ASSERT_OK( + authzSession->addAndAuthorizeUser(_opCtx.get(), kTenant1UserTestRequest, expire)); + ASSERT_EQ(authzSession->getExpiration(), expect); + + // Reset for next test. + VTS::set(_opCtx.get(), boost::none); + authzSession->startRequest(_opCtx.get()); + assertLogout(testTenant1FooCollResource, ActionType::insert); + }; + const auto exp = validatedTenancyScope.getExpiration(); + checkExpiration(boost::none, exp); // Uses token's expiration + checkExpiration(Date_t::max(), exp); // Longer expiration does not override token. + checkExpiration(exp - Seconds{1}, exp - Seconds{1}); // Shorter expiration does. + } + + { + VTS validatedTenancyScope(kTenant1UserTest, kVTSKey, VTS::TokenForTestingTag{}); + + // Perform authentication checks. + VTS::set(_opCtx.get(), validatedTenancyScope); ASSERT_OK( authzSession->addAndAuthorizeUser(_opCtx.get(), kTenant1UserTestRequest, boost::none)); @@ -1652,8 +1670,8 @@ TEST_F(AuthorizationSessionTest, ExpirationWithSecurityTokenNOK) { const auto kSomeCollNss = NamespaceString::createNamespaceString_forTest( boost::none, "anydb"_sd, "somecollection"_sd); const auto kSomeCollRsrc = ResourcePattern::forExactNamespace(kSomeCollNss); - ASSERT_OK( - authzSession->addAndAuthorizeUser(_opCtx.get(), kUser1TestRequest, expirationTime)); + ASSERT_OK(authzSession->addAndAuthorizeUser( + _opCtx.get(), kUser1TestRequest, Date_t() + Hours{1})); assertActive(kSomeCollRsrc, ActionType::insert); // Check that logout proceeds normally. @@ -1661,11 +1679,10 @@ TEST_F(AuthorizationSessionTest, ExpirationWithSecurityTokenNOK) { _client.get(), kTestDB, "Log out readWriteAny user for test"_sd); assertLogout(kSomeCollRsrc, ActionType::insert); } + // Create a new validated tenancy scope for the readWriteAny tenant user. { - VTS validatedTenancyScope = - VTS(BSON(authUserFieldName << kTenant2UserTest.toBSON(true /* encodeTenant */)), - VTS::TokenForTestingTag{}); + VTS validatedTenancyScope(kTenant2UserTest, kVTSKey, VTS::TokenForTestingTag{}); VTS::set(_opCtx.get(), validatedTenancyScope); ASSERT_OK( diff --git a/src/mongo/db/auth/validated_tenancy_scope.cpp b/src/mongo/db/auth/validated_tenancy_scope.cpp index 5364f2cea65..24ac4c69526 100644 --- a/src/mongo/db/auth/validated_tenancy_scope.cpp +++ b/src/mongo/db/auth/validated_tenancy_scope.cpp @@ -44,12 +44,14 @@ #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/bsontypes.h" +#include "mongo/bson/json.h" #include "mongo/crypto/hash_block.h" +#include "mongo/crypto/jwt_types_gen.h" #include "mongo/crypto/sha256_block.h" #include "mongo/db/auth/action_type.h" #include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/resource_pattern.h" -#include "mongo/db/auth/security_token_gen.h" +#include "mongo/db/auth/validated_tenancy_scope_gen.h" #include "mongo/db/client.h" #include "mongo/db/feature_flag.h" #include "mongo/db/multitenancy_gen.h" @@ -61,6 +63,7 @@ #include "mongo/logv2/log_component.h" #include "mongo/logv2/log_detail.h" #include "mongo/util/assert_util.h" +#include "mongo/util/base64.h" #include "mongo/util/decorable.h" #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kAccessControl @@ -69,6 +72,14 @@ namespace mongo::auth { namespace { const auto validatedTenancyScopeDecoration = OperationContext::declareDecoration>(); + +// Signed auth tokens are for internal testing only, and require the use of a preshared key. +// These tokens will have fixed values for kid/iss/aud fields. +// This usage will be replaced by full OIDC processing at a later time. +constexpr auto kTestOnlyKeyId = "test-only-kid"_sd; +constexpr auto kTestOnlyIssuer = "mongodb://test.kernel.localhost"_sd; +constexpr auto kTestOnlyAudience = "mongod-testing"_sd; + MONGO_INITIALIZER(SecurityTokenOptionValidate)(InitializerContext*) { if (gMultitenancySupport) { logv2::detail::setGetTenantIDCallback([]() -> std::string { @@ -93,32 +104,116 @@ MONGO_INITIALIZER(SecurityTokenOptionValidate)(InitializerContext*) { "featureFlagSecurityToken is enabled. This flag MUST NOT be enabled in production"); } } + +struct ParsedTokenView { + StringData header; + StringData body; + StringData signature; + + StringData payload; +}; + +// Split "header.body.signature" into {"header", "body", "signature", "header.body"} +ParsedTokenView parseSignedToken(StringData token) { + ParsedTokenView pt; + + auto split = token.find('.', 0); + uassert(8039404, "Missing JWS delimiter", split != std::string::npos); + pt.header = token.substr(0, split); + auto pos = split + 1; + + split = token.find('.', pos); + uassert(8039405, "Missing JWS delimiter", split != std::string::npos); + pt.body = token.substr(pos, split - pos); + pt.payload = token.substr(0, split); + pos = split + 1; + + split = token.find('.', pos); + uassert(8039406, "Too many delimiters in JWS token", split == std::string::npos); + pt.signature = token.substr(pos); + return pt; +} + +BSONObj decodeJSON(StringData b64) try { return fromjson(base64url::decode(b64)); } catch (...) { + auto status = exceptionToStatus(); + uasserted(status.code(), "Unable to parse security token: {}"_format(status.reason())); +} + } // namespace -ValidatedTenancyScope::ValidatedTenancyScope(BSONObj obj, InitTag tag) : _originalToken(obj) { - const bool enabled = gMultitenancySupport && - gFeatureFlagSecurityToken.isEnabled(serverGlobalParams.featureCompatibility); +ValidatedTenancyScope::ValidatedTenancyScope(Client* client, StringData securityToken) + : _originalToken(securityToken.toString()) { uassert(ErrorCodes::InvalidOptions, "Multitenancy not enabled, refusing to accept securityToken", - enabled || (tag == InitTag::kInitForShell)); + gMultitenancySupport); - auto token = SecurityToken::parse(IDLParserContext{"Security Token"}, obj); - auto authenticatedUser = token.getAuthenticatedUser(); + IDLParserContext ctxt("securityToken"); + auto parsed = parseSignedToken(securityToken); + // Unsigned tenantId provided via highly privileged connection will respect tenantId field only. + if (parsed.signature.empty()) { + auto* as = AuthorizationSession::get(client); + uassert(ErrorCodes::Unauthorized, + "Use of unsigned security token requires useTenant privilege", + as->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(boost::none), ActionType::useTenant)); + auto jwt = crypto::JWT::parse(ctxt, decodeJSON(parsed.body)); + uassert(ErrorCodes::Unauthorized, + "Unsigned security token must contain a tenantId", + jwt.getTenantId() != boost::none); + _tenantOrUser = jwt.getTenantId().get(); + return; + } + + // Else, we expect this to be an HS256 token using a preshared secret. + uassert(ErrorCodes::Unauthorized, + "Signed authentication tokens are not accepted without feature flag opt-in", + gFeatureFlagSecurityToken.isEnabledAndIgnoreFCVUnsafeAtStartup()); + + uassert(ErrorCodes::OperationFailed, + "Unable to validate test tokens when testOnlyValidatedTenancyScopeKey is not provided", + !gTestOnlyValidatedTenancyScopeKey.empty()); + StringData secret(gTestOnlyValidatedTenancyScopeKey); + + auto header = crypto::JWSHeader::parse(ctxt, decodeJSON(parsed.header)); uassert(ErrorCodes::BadValue, - "Security token authenticated user requires a valid Tenant ID", - authenticatedUser.getTenant()); + "Security token must be signed using 'HS256' algorithm", + header.getAlgorithm() == "HS256"_sd); - // Use actual authenticatedUser object as passed to preserve hash input. - auto authUserObj = obj[SecurityToken::kAuthenticatedUserFieldName].Obj(); - ConstDataRange authUserCDR(authUserObj.objdata(), authUserObj.objsize()); + auto computed = + SHA256Block::computeHmac(reinterpret_cast(secret.rawData()), + secret.size(), + reinterpret_cast(parsed.payload.rawData()), + parsed.payload.size()); + auto sigraw = base64url::decode(parsed.signature); + auto signature = SHA256Block::fromBuffer(reinterpret_cast(sigraw.data()), + sigraw.size()); - // Placeholder algorithm. - auto computed = SHA256Block::computeHash({authUserCDR}); + uassert(ErrorCodes::Unauthorized, "Token signature invalid", computed == signature); - uassert(ErrorCodes::Unauthorized, "Token signature invalid", computed == token.getSig()); + auto jwt = crypto::JWT::parse(ctxt, decodeJSON(parsed.body)); - _tenantOrUser = std::move(authenticatedUser); + // Expected hard-coded values for kid/iss/aud. + // These signed tokens are used exclusively by internal testing, + // and should not ever have different values than what we create. + uassert(ErrorCodes::BadValue, + "Security token must use kid == '{}'"_format(kTestOnlyKeyId), + header.getKeyId() == kTestOnlyKeyId); + uassert(ErrorCodes::BadValue, + "Security token must use iss == '{}'"_format(kTestOnlyIssuer), + jwt.getIssuer() == kTestOnlyIssuer); + uassert(ErrorCodes::BadValue, + "Security token must use aud == '{}'"_format(kTestOnlyAudience), + stdx::holds_alternative(jwt.getAudience())); + uassert(ErrorCodes::BadValue, + "Security token must use aud == '{}'"_format(kTestOnlyAudience), + stdx::get(jwt.getAudience()) == kTestOnlyAudience); + + auto swUserName = UserName::parse(jwt.getSubject(), jwt.getTenantId()); + uassertStatusOK(swUserName.getStatus().withContext("Invalid subject name")); + + _tenantOrUser = std::move(swUserName.getValue()); + _expiration = jwt.getExpiration(); } ValidatedTenancyScope::ValidatedTenancyScope(Client* client, TenantId tenant) @@ -141,17 +236,16 @@ ValidatedTenancyScope::ValidatedTenancyScope(Client* client, TenantId tenant) boost::optional ValidatedTenancyScope::create(Client* client, BSONObj body, - BSONObj securityToken) { + StringData securityToken) { if (!gMultitenancySupport) { return boost::none; } auto dollarTenantElem = body["$tenant"_sd]; - const bool hasToken = securityToken.nFields() > 0; uassert(6545800, "Cannot pass $tenant id if also passing securityToken", - dollarTenantElem.eoo() || !hasToken); + dollarTenantElem.eoo() || securityToken.empty()); uassert(ErrorCodes::OperationFailed, "Cannot process $tenant id when no client is available", dollarTenantElem.eoo() || client); @@ -163,8 +257,8 @@ boost::optional ValidatedTenancyScope::create(Client* cli if (dollarTenantElem) { return ValidatedTenancyScope(client, TenantId::parseFromBSON(dollarTenantElem)); - } else if (hasToken) { - return ValidatedTenancyScope(securityToken); + } else if (!securityToken.empty()) { + return ValidatedTenancyScope(client, securityToken); } else { return boost::none; } @@ -188,22 +282,66 @@ void ValidatedTenancyScope::set(OperationContext* opCtx, validatedTenancyScopeDecoration(opCtx) = std::move(token); } -ValidatedTenancyScope::ValidatedTenancyScope(BSONObj obj, TokenForTestingTag) { - auto authUserElem = obj[SecurityToken::kAuthenticatedUserFieldName]; - uassert(ErrorCodes::BadValue, - "Invalid field(s) in token being signed", - (authUserElem.type() == Object) && (obj.nFields() == 1)); - auto authUserObj = authUserElem.Obj(); - ConstDataRange authUserCDR(authUserObj.objdata(), authUserObj.objsize()); +ValidatedTenancyScope::ValidatedTenancyScope(const UserName& username, + StringData secret, + TokenForTestingTag tag) + : ValidatedTenancyScope(username, secret, Date_t::now() + kDefaultExpiration, tag) {} - // Placeholder algorithm. - auto sig = SHA256Block::computeHash({authUserCDR}); +ValidatedTenancyScope::ValidatedTenancyScope(const UserName& username, + StringData secret, + Date_t expiration, + TokenForTestingTag) { + invariant(!secret.empty()); - BSONObjBuilder signedToken(obj); - signedToken.appendBinData(SecurityToken::kSigFieldName, sig.size(), BinDataGeneral, sig.data()); - _originalToken = signedToken.obj(); - _tenantOrUser = UserName::parseFromBSONObj(authUserObj); + crypto::JWSHeader header; + header.setType("JWT"_sd); + header.setAlgorithm("HS256"_sd); + header.setKeyId(kTestOnlyKeyId); + + crypto::JWT body; + body.setIssuer(kTestOnlyIssuer); + body.setSubject(username.getUnambiguousName()); + body.setAudience(kTestOnlyAudience.toString()); + body.setTenantId(username.getTenant()); + body.setExpiration(std::move(expiration)); + + std::string payload = "{}.{}"_format(base64url::encode(tojson(header.toBSON())), + base64url::encode(tojson(body.toBSON()))); + + auto computed = + SHA256Block::computeHmac(reinterpret_cast(secret.rawData()), + secret.size(), + reinterpret_cast(payload.data()), + payload.size()); + + _originalToken = + "{}.{}"_format(payload, + base64url::encode(StringData(reinterpret_cast(computed.data()), + computed.size()))); + + if (gTestOnlyValidatedTenancyScopeKey == secret) { + _tenantOrUser = username; + _expiration = body.getExpiration(); + } +} + +ValidatedTenancyScope::ValidatedTenancyScope(TenantId tenant, TenantForTestingTag) { + crypto::JWSHeader header; + header.setType("JWT"_sd); + header.setAlgorithm("none"_sd); + header.setKeyId("none"_sd); + + crypto::JWT body; + body.setIssuer("mongodb://testing.localhost"_sd); + body.setSubject("."); + body.setAudience(std::string{"mongod-testing"}); + body.setTenantId(tenant); + body.setExpiration(Date_t::max()); + + _originalToken = "{}.{}."_format(base64url::encode(tojson(header.toBSON())), + base64url::encode(tojson(body.toBSON()))); + _tenantOrUser = std::move(tenant); } } // namespace mongo::auth diff --git a/src/mongo/db/auth/validated_tenancy_scope.h b/src/mongo/db/auth/validated_tenancy_scope.h index 0742a01cb9f..74c4accc3ca 100644 --- a/src/mongo/db/auth/validated_tenancy_scope.h +++ b/src/mongo/db/auth/validated_tenancy_scope.h @@ -39,6 +39,7 @@ #include "mongo/db/tenant_id.h" #include "mongo/stdx/variant.h" #include "mongo/util/overloaded_visitor.h" // IWYU pragma: keep +#include "mongo/util/time_support.h" namespace mongo { @@ -52,18 +53,11 @@ public: ValidatedTenancyScope() = delete; ValidatedTenancyScope(const ValidatedTenancyScope&) = default; - // kInitForShell allows parsing a securityToken without multitenancy enabled. - // This is required in the shell since we do not enable this setting in non-servers. - enum class InitTag { - kNormal, - kInitForShell, - }; - /** - * Constructs a ValidatedTenancyScope by parsing a SecurityToken from a BSON object + * Constructs a ValidatedTenancyScope by parsing a SecurityToken from a JWS String * and verifying its cryptographic signature. */ - explicit ValidatedTenancyScope(BSONObj securityToken, InitTag tag = InitTag::kNormal); + ValidatedTenancyScope(Client* client, StringData securityToken); /** * Constructs a ValidatedTenancyScope for tenant only by validating that the @@ -79,25 +73,39 @@ public: */ static boost::optional create(Client* client, BSONObj body, - BSONObj securityToken); + StringData securityToken); bool hasAuthenticatedUser() const; const UserName& authenticatedUser() const; + bool hasTenantId() const { + return stdx::visit(OverloadedVisitor{ + [](const std::monostate&) { return false; }, + [](const UserName& userName) { return !!userName.getTenant(); }, + [](const TenantId& tenant) { return true; }, + }, + _tenantOrUser); + } + const TenantId& tenantId() const { return stdx::visit( OverloadedVisitor{ + [](const std::monostate&) -> const TenantId& { MONGO_UNREACHABLE; }, [](const UserName& userName) -> decltype(auto) { return *userName.getTenant(); }, [](const TenantId& tenant) -> decltype(auto) { return tenant; }, }, _tenantOrUser); } - BSONObj getOriginalToken() const { + StringData getOriginalToken() const { return _originalToken; } + Date_t getExpiration() const { + return _expiration; + } + /** * Get/Set a ValidatedTenancyScope as a decoration on the OperationContext */ @@ -108,14 +116,26 @@ public: * Transitional token generator, do not use outside of test code. */ struct TokenForTestingTag {}; - explicit ValidatedTenancyScope(BSONObj token, TokenForTestingTag); + static constexpr Minutes kDefaultExpiration{15}; + explicit ValidatedTenancyScope(const UserName& username, StringData secret, TokenForTestingTag); + explicit ValidatedTenancyScope(const UserName& username, + StringData secret, + Date_t expiration, + TokenForTestingTag); /** * Setup a validated tenant for test, do not use outside of test code. */ struct TenantForTestingTag {}; - explicit ValidatedTenancyScope(TenantId tenant, TenantForTestingTag) - : _tenantOrUser(std::move(tenant)) {} + explicit ValidatedTenancyScope(TenantId tenant, TenantForTestingTag); + + /** + * Initializes a VTS object with original BSON only. + * Used by shell to prepare outgoing OpMsg requests. + */ + struct InitForShellTag {}; + explicit ValidatedTenancyScope(std::string token, InitForShellTag) + : _originalToken(std::move(token)) {} /** * Backdoor API to setup a validated tenant. For use only when a security context is not @@ -127,9 +147,15 @@ public: private: // Preserve original token for serializing from MongoQ. - BSONObj _originalToken; + std::string _originalToken; - stdx::variant _tenantOrUser; + // Expiration time if any. + Date_t _expiration = Date_t::max(); + + // monostate represents a VTS which has not actually been validated. + // It should only persist into construction within the shell, + // where VTS is used for sending token data to a server via _originalBSON. + stdx::variant _tenantOrUser; }; } // namespace auth diff --git a/src/mongo/db/auth/security_token.idl b/src/mongo/db/auth/validated_tenancy_scope.idl similarity index 72% rename from src/mongo/db/auth/security_token.idl rename to src/mongo/db/auth/validated_tenancy_scope.idl index 133d67eb913..e77440b4e4f 100644 --- a/src/mongo/db/auth/security_token.idl +++ b/src/mongo/db/auth/validated_tenancy_scope.idl @@ -30,20 +30,12 @@ global: cpp_namespace: "mongo::auth" imports: - - "mongo/db/auth/auth_types.idl" - - "mongo/crypto/sha256_block.idl" - "mongo/db/basic_types.idl" -structs: - SecurityToken: - description: "Security Token as passed in OP_MSG" - strict: true - fields: - authenticatedUser: - description: Authenticated user for which this token grants authorizations - type: UserName - sig: - # WIP This is temporarily a SHA256 hash of the authenticatedUser BSON object. - description: Validated signature on this security token - type: sha256Block - +server_parameters: + testOnlyValidatedTenancyScopeKey: + description: "HS256 preshared secret for making self-signed security tokens" + set_at: startup + cpp_vartype: std::string + cpp_varname: gTestOnlyValidatedTenancyScopeKey + test_only: true diff --git a/src/mongo/db/auth/validated_tenancy_scope_test.cpp b/src/mongo/db/auth/validated_tenancy_scope_test.cpp index 1ab4b712069..3e10849328e 100644 --- a/src/mongo/db/auth/validated_tenancy_scope_test.cpp +++ b/src/mongo/db/auth/validated_tenancy_scope_test.cpp @@ -53,7 +53,6 @@ #include "mongo/db/auth/privilege.h" #include "mongo/db/auth/resource_pattern.h" #include "mongo/db/auth/role_name.h" -#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/user.h" #include "mongo/db/auth/validated_tenancy_scope.h" #include "mongo/db/client.h" @@ -101,13 +100,9 @@ protected: client = getServiceContext()->makeClient("test"); } - BSONObj makeSecurityToken(const UserName& userName) { - constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName; - auto authUser = userName.toBSON(true /* serialize token */); - ASSERT_EQ(authUser["tenant"_sd].type(), jstOID); + std::string makeSecurityToken(const UserName& userName) { using VTS = auth::ValidatedTenancyScope; - return VTS(BSON(authUserFieldName << authUser), VTS::TokenForTestingTag{}) - .getOriginalToken(); + return VTS(userName, "secret"_sd, VTS::TokenForTestingTag{}).getOriginalToken().toString(); } ServiceContext::UniqueClient client; @@ -137,6 +132,8 @@ TEST_F(ValidatedTenancyScopeTestFixture, MultitenancySupportWithTenantOK) { TEST_F(ValidatedTenancyScopeTestFixture, MultitenancySupportWithSecurityTokenOK) { RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + RAIIServerParameterControllerForTest secretController("testOnlyValidatedTenancyScopeKey", + "secret"); const TenantId kTenantId(OID::gen()); auto body = BSON("ping" << 1); @@ -145,6 +142,7 @@ TEST_F(ValidatedTenancyScopeTestFixture, MultitenancySupportWithSecurityTokenOK) auto validated = ValidatedTenancyScope::create(client.get(), body, token); ASSERT_TRUE(validated != boost::none); + ASSERT_TRUE(validated->hasTenantId()); ASSERT_TRUE(validated->tenantId() == kTenantId); ASSERT_TRUE(validated->hasAuthenticatedUser()); ASSERT_TRUE(validated->authenticatedUser() == user); @@ -171,7 +169,7 @@ TEST_F(ValidatedTenancyScopeTestFixture, MultitenancySupportWithTenantNOK) { ASSERT_THROWS_CODE( ValidatedTenancyScope(client.get(), TenantId(kOid)), DBException, ErrorCodes::Unauthorized); - ASSERT_THROWS_CODE(ValidatedTenancyScope::create(client.get(), body, {}), + ASSERT_THROWS_CODE(ValidatedTenancyScope::create(client.get(), body, ""_sd), DBException, ErrorCodes::Unauthorized); } @@ -198,6 +196,33 @@ TEST_F(ValidatedTenancyScopeTestFixture, MultitenancySupportWithTenantAndSecurit ValidatedTenancyScope::create(client.get(), body, token), DBException, 6545800); } +TEST_F(ValidatedTenancyScopeTestFixture, NoScopeKey) { + RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); + RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + + UserName user("user", "admin", TenantId(OID::gen())); + auto token = makeSecurityToken(user); + ASSERT_THROWS_CODE_AND_WHAT( + ValidatedTenancyScope(client.get(), token), + DBException, + ErrorCodes::OperationFailed, + "Unable to validate test tokens when testOnlyValidatedTenancyScopeKey is not provided"); +} + +TEST_F(ValidatedTenancyScopeTestFixture, WrongScopeKey) { + RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); + RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + RAIIServerParameterControllerForTest secretController("testOnlyValidatedTenancyScopeKey", + "password"); // != "secret" + + UserName user("user", "admin", TenantId(OID::gen())); + auto token = makeSecurityToken(user); + ASSERT_THROWS_CODE_AND_WHAT(ValidatedTenancyScope(client.get(), token), + DBException, + ErrorCodes::Unauthorized, + "Token signature invalid"); +} + } // namespace } // namespace auth } // namespace mongo diff --git a/src/mongo/db/basic_types.idl b/src/mongo/db/basic_types.idl index ce52cc210b7..815990935f6 100644 --- a/src/mongo/db/basic_types.idl +++ b/src/mongo/db/basic_types.idl @@ -319,6 +319,13 @@ types: deserializer: "mongo::TenantId::parseFromBSON" serializer: "mongo::TenantId::serializeToBSON" + tenant_id_hex: + bson_serialization_type: string + description: "A 24 hexit string representing a tenantId" + cpp_type: "TenantId" + deserializer: "mongo::TenantId::parseFromString" + serializer: "mongo::TenantId::toString" + database_name: bson_serialization_type: string description: "A MongoDB DatabaseName" diff --git a/src/mongo/embedded/embedded_auth_session.cpp b/src/mongo/embedded/embedded_auth_session.cpp index e9308e9c0a5..1b44a96bb62 100644 --- a/src/mongo/embedded/embedded_auth_session.cpp +++ b/src/mongo/embedded/embedded_auth_session.cpp @@ -264,6 +264,10 @@ public: return false; } + const boost::optional& getExpiration() const override { + UASSERT_NOT_IMPLEMENTED; + } + protected: std::tuple*, std::vector*> _getImpersonations() override { UASSERT_NOT_IMPLEMENTED; diff --git a/src/mongo/idl/SConscript b/src/mongo/idl/SConscript index 0dccb85434a..60c8ce4cfe1 100644 --- a/src/mongo/idl/SConscript +++ b/src/mongo/idl/SConscript @@ -167,6 +167,7 @@ env.CppUnitTest( ], LIBDEPS=[ '$BUILD_DIR/mongo/db/auth/authprivilege', + '$BUILD_DIR/mongo/db/auth/security_token', '$BUILD_DIR/mongo/db/server_base', '$BUILD_DIR/mongo/db/server_feature_flags', '$BUILD_DIR/mongo/db/service_context', diff --git a/src/mongo/rpc/metadata/security_token_metadata_test.cpp b/src/mongo/rpc/metadata/security_token_metadata_test.cpp index ec8dc077e67..56e3c3420e4 100644 --- a/src/mongo/rpc/metadata/security_token_metadata_test.cpp +++ b/src/mongo/rpc/metadata/security_token_metadata_test.cpp @@ -43,7 +43,6 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/bsontypes.h" #include "mongo/bson/oid.h" -#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/auth/validated_tenancy_scope.h" #include "mongo/db/service_context.h" @@ -64,14 +63,9 @@ namespace { constexpr auto kPingFieldName = "ping"_sd; -BSONObj makeSecurityToken(const UserName& userName) { - constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName; - auto authUser = userName.toBSON(true /* serialize token */); - ASSERT_EQ(authUser["tenant"_sd].type(), jstOID); +std::string makeSecurityToken(const UserName& userName) { using VTS = auth::ValidatedTenancyScope; - return VTS(BSON(authUserFieldName << authUser), VTS::TokenForTestingTag{}) - .getOriginalToken() - .getOwned(); + return VTS(userName, "secret"_sd, VTS::TokenForTestingTag{}).getOriginalToken().toString(); } class SecurityTokenMetadataTest : public ServiceContextTest { @@ -83,6 +77,19 @@ protected: ServiceContext::UniqueClient client; }; +TEST_F(SecurityTokenMetadataTest, SecurityTokenSingletenancy) { + RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", false); + + const auto kPingBody = BSON(kPingFieldName << 1); + const auto kTokenBody = makeSecurityToken(UserName("user", "admin", TenantId(OID::gen()))); + + auto msgBytes = OpMsgBytes{0, kBodySection, kPingBody, kSecurityTokenSection, kTokenBody}; + ASSERT_THROWS_CODE_AND_WHAT(msgBytes.parse(), + DBException, + ErrorCodes::Unauthorized, + "Unsupported Security Token provided"); +} + TEST_F(SecurityTokenMetadataTest, SecurityTokenNotAccepted) { RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", false); @@ -91,15 +98,33 @@ TEST_F(SecurityTokenMetadataTest, SecurityTokenNotAccepted) { const auto kTokenBody = makeSecurityToken(UserName("user", "admin", TenantId(OID::gen()))); auto msgBytes = OpMsgBytes{0, kBodySection, kPingBody, kSecurityTokenSection, kTokenBody}; - ASSERT_THROWS_CODE_AND_WHAT(msgBytes.parse(), - DBException, - ErrorCodes::InvalidOptions, - "Multitenancy not enabled, refusing to accept securityToken"); + ASSERT_THROWS_CODE_AND_WHAT( + msgBytes.parse(), + DBException, + ErrorCodes::Unauthorized, + "Signed authentication tokens are not accepted without feature flag opt-in"); +} + +TEST_F(SecurityTokenMetadataTest, SecurityTokenTestTokensNotAvailable) { + RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); + RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + + const auto kPingBody = BSON(kPingFieldName << 1); + const auto kTokenBody = makeSecurityToken(UserName("user", "admin", TenantId(OID::gen()))); + + auto msgBytes = OpMsgBytes{0, kBodySection, kPingBody, kSecurityTokenSection, kTokenBody}; + ASSERT_THROWS_CODE_AND_WHAT( + msgBytes.parse(), + DBException, + ErrorCodes::OperationFailed, + "Unable to validate test tokens when testOnlyValidatedTenancyScopeKey is not provided"); } TEST_F(SecurityTokenMetadataTest, BasicSuccess) { RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + RAIIServerParameterControllerForTest secretController("testOnlyValidatedTenancyScopeKey", + "secret"); const auto kTenantId = TenantId(OID::gen()); const auto kPingBody = BSON(kPingFieldName << 1); @@ -109,7 +134,7 @@ TEST_F(SecurityTokenMetadataTest, BasicSuccess) { ASSERT_BSONOBJ_EQ(msg.body, kPingBody); ASSERT_EQ(msg.sequences.size(), 0u); ASSERT_TRUE(msg.validatedTenancyScope != boost::none); - ASSERT_BSONOBJ_EQ(msg.validatedTenancyScope->getOriginalToken(), kTokenBody); + ASSERT_EQ(msg.validatedTenancyScope->getOriginalToken(), kTokenBody); ASSERT_EQ(msg.validatedTenancyScope->tenantId(), kTenantId); auto opCtx = makeOperationContext(); diff --git a/src/mongo/rpc/op_msg.cpp b/src/mongo/rpc/op_msg.cpp index 45ebb2e1093..8b6049b01dd 100644 --- a/src/mongo/rpc/op_msg.cpp +++ b/src/mongo/rpc/op_msg.cpp @@ -177,7 +177,7 @@ OpMsg OpMsg::parse(const Message& message, Client* client) try { // comments. bool haveBody = false; OpMsg msg; - BSONObj securityToken; + StringData securityToken; while (!sectionsBuf.atEof()) { const auto sectionKind = sectionsBuf.read
(); switch (sectionKind) { @@ -221,7 +221,7 @@ OpMsg OpMsg::parse(const Message& message, Client* client) try { uassert(ErrorCodes::Unauthorized, "Unsupported Security Token provided", gMultitenancySupport); - securityToken = sectionsBuf.read>(); + securityToken = sectionsBuf.readCStr(); break; } @@ -423,7 +423,7 @@ void serializeHelper(const std::vector& sequences, OpMsgBuilder* output) { if (validatedTenancyScope) { auto securityToken = validatedTenancyScope->getOriginalToken(); - if (securityToken.nFields() > 0) { + if (!securityToken.empty()) { output->setSecurityToken(securityToken); } } @@ -462,15 +462,14 @@ void OpMsg::shareOwnershipWith(const ConstSharedBuffer& buffer) { } } -BSONObjBuilder OpMsgBuilder::beginSecurityToken() { +void OpMsgBuilder::setSecurityToken(StringData token) { invariant(_state == kEmpty); - _state = kSecurityToken; _buf.appendStruct(Section::kSecurityToken); - return BSONObjBuilder(_buf); + _buf.appendStr(token, true /* includeEndingNull */); } auto OpMsgBuilder::beginDocSequence(StringData name) -> DocSequenceBuilder { - invariant((_state == kEmpty) || (_state == kSecurityToken) || (_state == kDocSequence)); + invariant((_state == kEmpty) || (_state == kDocSequence)); invariant(!_openBuilder); _openBuilder = true; _state = kDocSequence; @@ -491,7 +490,7 @@ void OpMsgBuilder::finishDocumentStream(DocSequenceBuilder* docSequenceBuilder) } BSONObjBuilder OpMsgBuilder::beginBody() { - invariant((_state == kEmpty) || (_state == kSecurityToken) || (_state == kDocSequence)); + invariant((_state == kEmpty) || (_state == kDocSequence)); _state = kBody; _buf.appendStruct(Section::kBody); invariant(_bodyStart == 0); diff --git a/src/mongo/rpc/op_msg.h b/src/mongo/rpc/op_msg.h index 35f4ca98aef..30224d33e09 100644 --- a/src/mongo/rpc/op_msg.h +++ b/src/mongo/rpc/op_msg.h @@ -287,10 +287,7 @@ public: resumeBody().appendElements(body); } - BSONObjBuilder beginSecurityToken(); - void setSecurityToken(const BSONObj& token) { - beginSecurityToken().appendElements(token); - } + void setSecurityToken(StringData token); /** * Finish building and return a Message ready to give to the networking layer for transmission. @@ -353,7 +350,6 @@ private: kEmpty, kDocSequence, kBody, - kSecurityToken, kDone, }; diff --git a/src/mongo/rpc/op_msg_test.cpp b/src/mongo/rpc/op_msg_test.cpp index b0d08ff312c..5f707889aa5 100644 --- a/src/mongo/rpc/op_msg_test.cpp +++ b/src/mongo/rpc/op_msg_test.cpp @@ -59,7 +59,6 @@ #include "mongo/db/auth/privilege.h" #include "mongo/db/auth/resource_pattern.h" #include "mongo/db/auth/role_name.h" -#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/user.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/auth/validated_tenancy_scope.h" @@ -836,24 +835,21 @@ protected: client = getServiceContext()->makeClient("test"); } - BSONObj makeSecurityToken(const UserName& userName) { - constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName; - auto authUser = userName.toBSON(true /* serialize token */); - ASSERT_EQ(authUser["tenant"_sd].type(), jstOID); - using VTS = auth::ValidatedTenancyScope; - return VTS(BSON(authUserFieldName << authUser), VTS::TokenForTestingTag{}) - .getOriginalToken(); - } - ServiceContext::UniqueClient client; }; TEST_F(OpMsgWithAuth, ParseValidatedTenancyScopeFromSecurityToken) { RAIIServerParameterControllerForTest multitenancyController("multitenancySupport", true); RAIIServerParameterControllerForTest securityTokenController("featureFlagSecurityToken", true); + RAIIServerParameterControllerForTest secretController("testOnlyValidatedTenancyScopeKey", + "secret"); + using VTS = auth::ValidatedTenancyScope; const auto kTenantId = TenantId(OID::gen()); - const auto token = makeSecurityToken(UserName("user", "admin", kTenantId)); + const auto token = + VTS(UserName("user", "admin", kTenantId), "secret"_sd, VTS::TokenForTestingTag{}) + .getOriginalToken() + .toString(); auto msg = OpMsgBytes{ kNoFlags, // diff --git a/src/mongo/scripting/mozjs/mongo.cpp b/src/mongo/scripting/mozjs/mongo.cpp index 25ee2282465..32a56e0cc77 100644 --- a/src/mongo/scripting/mozjs/mongo.cpp +++ b/src/mongo/scripting/mozjs/mongo.cpp @@ -374,15 +374,14 @@ void doRunCommand(JSContext* cx, JS::CallArgs args, MakeRequest makeRequest) { auto arg = ValueWriter(cx, args.get(1)).toBSON(); auto request = makeRequest(database, arg); - if (auto tokenArg = args.get(3); tokenArg.isObject()) { + if (auto tokenArg = args.get(3); tokenArg.isString()) { using VTS = auth::ValidatedTenancyScope; - if (auto token = ValueWriter(cx, tokenArg).toBSON(); token.nFields() > 0) { - request.validatedTenancyScope = VTS(token, VTS::InitTag::kInitForShell); + if (auto token = ValueWriter(cx, tokenArg).toString(); !token.empty()) { + request.validatedTenancyScope = VTS(token, VTS::InitForShellTag{}); } } else { uassert(ErrorCodes::BadValue, - str::stream() << "The token parameter to " << Params::kCommandName - << " must be an object", + "The token parameter to {} must be a string"_format(Params::kCommandName), tokenArg.isUndefined()); } diff --git a/src/mongo/shell/shell_utils.cpp b/src/mongo/shell/shell_utils.cpp index 4c8e8f3ee40..99831659e77 100644 --- a/src/mongo/shell/shell_utils.cpp +++ b/src/mongo/shell/shell_utils.cpp @@ -72,7 +72,6 @@ #include "mongo/config.h" // IWYU pragma: keep #include "mongo/crypto/hash_block.h" #include "mongo/crypto/sha256_block.h" -#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/validated_tenancy_scope.h" #include "mongo/db/database_name.h" #include "mongo/db/hasher.h" @@ -459,17 +458,35 @@ BSONObj convertShardKeyToHashed(const BSONObj& a, void* data) { * Generate a security token suitable for passing in an OpMsg payload token field. * * @param user object - { user: 'name', db: 'dbname', tenant: OID } - * @return object - { authenticatedUser: {...user object...}, sig: BinDataGeneral(Signature) } + * @param secret string - Secret to use for test signing + * @return string - Compact serialized JWS on an OIDC token. */ BSONObj _createSecurityToken(const BSONObj& args, void* data) { + std::vector argv; + args.elems(argv); uassert(6161500, - "_createSecurityToken requires a single object argument", - (args.nFields() == 1) && (args.firstElement().type() == Object)); + "_createSecurityToken requires two arguments, an object and a non-empty string", + (argv.size() == 2) && (argv[0].type() == Object) && (argv[1].type() == String) && + !argv[1].valueStringData().empty()); - constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName; - auto authUser = args.firstElement().Obj(); using VTS = auth::ValidatedTenancyScope; - auto token = VTS(BSON(authUserFieldName << authUser), VTS::TokenForTestingTag{}); + auto token = + VTS(UserName::parseFromBSON(argv[0]), argv[1].valueStringData(), VTS::TokenForTestingTag{}); + return BSON("" << token.getOriginalToken()); +} + +/** + * Generate an unsigned security token which contains a tenant component. + * @param tenant OID - The tenantId. + * @return string - Unsigned compact serialized JWS on an OIDC token. + */ +BSONObj _createTenantToken(const BSONObj& args, void* data) { + uassert(8039400, + "_createTenantToken requires an objectid", + (args.nFields() == 1) && (args.firstElement().type() == jstOID)); + + using VTS = auth::ValidatedTenancyScope; + auto token = VTS(TenantId{args.firstElement().OID()}, VTS::TenantForTestingTag{}); return BSON("" << token.getOriginalToken()); } @@ -758,6 +775,7 @@ BSONObj _fnvHashToHexString(const BSONObj& args, void*) { void installShellUtils(Scope& scope) { scope.injectNative("getMemInfo", JSGetMemInfo); scope.injectNative("_createSecurityToken", _createSecurityToken); + scope.injectNative("_createTenantToken", _createTenantToken); scope.injectNative("_replMonitorStats", replMonitorStats); scope.injectNative("_srand", JSSrand); scope.injectNative("_rand", JSRand);