0
0
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:
Sara Golemon 2023-08-23 10:34:05 -05:00 committed by Evergreen Agent
parent f0af4e65f3
commit f8befd2f22
36 changed files with 568 additions and 227 deletions

View File

@ -101,6 +101,7 @@ globals:
isObject: true
isString: true
_createSecurityToken: true
_createTenantToken: true
_isAddressSanitizerActive: true
_isLeakSanitizerActive: true
_isThreadSanitizerActive: true

View File

@ -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

View File

@ -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.

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -37,7 +37,8 @@ rst.startSet({
setParameter: {
multitenancySupport: true,
featureFlagRequireTenantID: true,
featureFlagSecurityToken: true
featureFlagSecurityToken: true,
testOnlyValidatedTenancyScopeKey: ChangeStreamMultitenantReplicaSetTest.getTokenKey(),
}
});
rst.initiate();

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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',
],
)

View File

@ -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();

View File

@ -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

View File

@ -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',

View File

@ -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;
};

View File

@ -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();

View File

@ -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;

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -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',

View File

@ -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();

View File

@ -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);

View File

@ -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,
};

View File

@ -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, //

View File

@ -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());
}

View File

@ -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);