mirror of
https://github.com/mongodb/mongo.git
synced 2024-11-21 12:39:08 +01:00
SERVER-80394 Improve securityToken handling
This commit is contained in:
parent
f0af4e65f3
commit
f8befd2f22
@ -101,6 +101,7 @@ globals:
|
||||
isObject: true
|
||||
isString: true
|
||||
_createSecurityToken: true
|
||||
_createTenantToken: true
|
||||
_isAddressSanitizerActive: true
|
||||
_isLeakSanitizerActive: true
|
||||
_isThreadSanitizerActive: true
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
replSetTest.stopSet();
|
||||
|
@ -37,7 +37,8 @@ rst.startSet({
|
||||
setParameter: {
|
||||
multitenancySupport: true,
|
||||
featureFlagRequireTenantID: true,
|
||||
featureFlagSecurityToken: true
|
||||
featureFlagSecurityToken: true,
|
||||
testOnlyValidatedTenancyScopeKey: ChangeStreamMultitenantReplicaSetTest.getTokenKey(),
|
||||
}
|
||||
});
|
||||
rst.initiate();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
runTestNoMultiTenancySupport();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
rst.stopSet();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
killCurrentOpTest();
|
||||
|
@ -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',
|
||||
],
|
||||
)
|
||||
|
@ -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<std::string> 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();
|
||||
|
@ -111,6 +111,11 @@ structs:
|
||||
type:
|
||||
variant: [string, array<string>]
|
||||
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
|
||||
|
@ -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',
|
||||
|
@ -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<Date_t>& getExpiration() const = 0;
|
||||
|
||||
protected:
|
||||
virtual std::tuple<boost::optional<UserName>*, std::vector<RoleName>*> _getImpersonations() = 0;
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -179,6 +179,9 @@ public:
|
||||
bool mayBypassWriteBlockingMode() const override;
|
||||
|
||||
bool isExpired() const override;
|
||||
const boost::optional<Date_t>& getExpiration() const override {
|
||||
return _expirationTime;
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class AuthorizationSessionImplTestHelper;
|
||||
|
@ -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<Date_t>& 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(
|
||||
|
@ -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<boost::optional<ValidatedTenancyScope>>();
|
||||
|
||||
// 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<const std::uint8_t*>(secret.rawData()),
|
||||
secret.size(),
|
||||
reinterpret_cast<const std::uint8_t*>(parsed.payload.rawData()),
|
||||
parsed.payload.size());
|
||||
auto sigraw = base64url::decode(parsed.signature);
|
||||
auto signature = SHA256Block::fromBuffer(reinterpret_cast<const std::uint8_t*>(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<std::string>(jwt.getAudience()));
|
||||
uassert(ErrorCodes::BadValue,
|
||||
"Security token must use aud == '{}'"_format(kTestOnlyAudience),
|
||||
stdx::get<std::string>(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> 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> 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<const std::uint8_t*>(secret.rawData()),
|
||||
secret.size(),
|
||||
reinterpret_cast<const std::uint8_t*>(payload.data()),
|
||||
payload.size());
|
||||
|
||||
_originalToken =
|
||||
"{}.{}"_format(payload,
|
||||
base64url::encode(StringData(reinterpret_cast<const char*>(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
|
||||
|
@ -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<ValidatedTenancyScope> 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<UserName, TenantId> _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<std::monostate, UserName, TenantId> _tenantOrUser;
|
||||
};
|
||||
|
||||
} // namespace auth
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -264,6 +264,10 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
const boost::optional<Date_t>& getExpiration() const override {
|
||||
UASSERT_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::tuple<boost::optional<UserName>*, std::vector<RoleName>*> _getImpersonations() override {
|
||||
UASSERT_NOT_IMPLEMENTED;
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
|
@ -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<Section>();
|
||||
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<Validated<BSONObj>>();
|
||||
securityToken = sectionsBuf.readCStr();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -423,7 +423,7 @@ void serializeHelper(const std::vector<OpMsg::DocumentSequence>& 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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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, //
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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<BSONElement> 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);
|
||||
|
Loading…
Reference in New Issue
Block a user