diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2021-12-13 18:32:55 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-12-29 17:43:13 +0000 |
commit | fd163ac958a6263a89b55faed127eceff9fea2f0 (patch) | |
tree | 1df62ed2676f33eec8e9305fbedecd5aa51bb9a0 | |
parent | a8305a25b2b5627e42c180279a870c50780df28b (diff) | |
download | mongo-fd163ac958a6263a89b55faed127eceff9fea2f0.tar.gz |
SERVER-61617 Restrict actionType/matchType combinations on serverless
-rw-r--r-- | buildscripts/idl/idl/enum_types.py | 4 | ||||
-rw-r--r-- | jstests/auth/token_privileges.js | 80 | ||||
-rw-r--r-- | src/mongo/db/auth/action_type.idl | 183 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_impl.cpp | 73 | ||||
-rw-r--r-- | src/mongo/db/auth/resource_pattern.h | 1 |
5 files changed, 316 insertions, 25 deletions
diff --git a/buildscripts/idl/idl/enum_types.py b/buildscripts/idl/idl/enum_types.py index 4098968ea10..c697cd775e4 100644 --- a/buildscripts/idl/idl/enum_types.py +++ b/buildscripts/idl/idl/enum_types.py @@ -188,6 +188,10 @@ class EnumTypeInfoBase(object, metaclass=ABCMeta): # One or more enums does not have associated extra data. indented_writer.write_line('default: return BSONObj();') + if len(extra_values) == len(self._enum.values): + # All enum cases handled, the compiler should know this. + indented_writer.write_line('MONGO_UNREACHABLE;') + class _EnumTypeInt(EnumTypeInfoBase, metaclass=ABCMeta): """Type information for integer enumerations.""" diff --git a/jstests/auth/token_privileges.js b/jstests/auth/token_privileges.js new file mode 100644 index 00000000000..0af38e4777e --- /dev/null +++ b/jstests/auth/token_privileges.js @@ -0,0 +1,80 @@ +// Test role restrictions when using security tokens. +// @tags: [requires_replication] + +(function() { +'use strict'; + +const tenantID = ObjectId(); +const isMongoStoreEnabled = TestData.setParameters.featureFlagMongoStore; + +if (!isMongoStoreEnabled) { + assert.throws(() => MongoRunner.runMongod({ + setParameter: "multitenancySupport=true", + })); + return; +} + +function runTest(conn, rst = undefined) { + const admin = conn.getDB('admin'); + const external = conn.getDB('$external'); + assert.commandWorked(admin.runCommand({createUser: 'admin', pwd: 'admin', roles: ['root']})); + assert(admin.auth('admin', 'admin')); + + // Create tenant-specific users. + const users = { + readOnlyUser: {roles: [{role: 'readAnyDatabase', db: 'admin'}]}, + readWriteUser: {roles: [{role: 'readWriteAnyDatabase', db: 'admin'}]}, + clusterAdminUser: {roles: [{role: 'clusterAdmin', db: 'admin'}], prohibited: true}, + }; + Object.keys(users).forEach( + (user) => assert.commandWorked(external.runCommand( + {createUser: user, '$tenant': tenantID, roles: users[user].roles}))); + if (rst) { + rst.awaitReplication(); + } + + Object.keys(users).forEach(function(user) { + const tokenConn = new Mongo(conn.host); + tokenConn._setSecurityToken( + _createSecurityToken({user: user, db: '$external', tenant: tenantID})); + const tokenDB = tokenConn.getDB('test'); + if (users[user].prohibited) { + assert.commandFailed(tokenDB.adminCommand({connectionStatus: 1})); + } else { + const authInfo = + assert.commandWorked(tokenDB.adminCommand({connectionStatus: 1})).authInfo; + jsTest.log(authInfo); + + assert.eq(authInfo.authenticatedUsers.length, 1); + assert.eq(authInfo.authenticatedUsers[0].user, user); + assert.eq(authInfo.authenticatedUsers[0].db, '$external'); + + const authedRoles = + authInfo.authenticatedUserRoles.map((role) => role.db + '.' + role.role); + const expectRoles = users[user].roles.map((role) => role.db + '.' + role.role); + const unexpectedRoles = authedRoles.filter((role) => !expectRoles.includes(role)); + assert.eq(unexpectedRoles.length, 0, 'Unexpected roles: ' + tojson(unexpectedRoles)); + const missingRoles = expectRoles.filter((role) => !authedRoles.includes(role)); + assert.eq(missingRoles.length, 0, 'Missing roles: ' + tojson(missingRoles)); + } + }); +} + +const opts = { + auth: '', + setParameter: "multitenancySupport=true", +}; +{ + const standalone = MongoRunner.runMongod(opts); + assert(standalone !== null, "MongoD failed to start"); + runTest(standalone); + MongoRunner.stopMongod(standalone); +} +{ + const rst = new ReplSetTest({nodes: 2, nodeOptions: opts}); + rst.startSet({keyFile: 'jstests/libs/key1'}); + rst.initiate(); + runTest(rst.getPrimary(), rst); + rst.stopSet(); +} +})(); diff --git a/src/mongo/db/auth/action_type.idl b/src/mongo/db/auth/action_type.idl index c789cceb004..03e23892083 100644 --- a/src/mongo/db/auth/action_type.idl +++ b/src/mongo/db/auth/action_type.idl @@ -35,6 +35,9 @@ global: cpp_namespace: "mongo" +imports: + - "mongo/idl/basic_types.idl" + enums: ActionType: description: "test" @@ -177,29 +180,163 @@ enums: viewUser : "viewUser" applyOps : "applyOps" + # In 'MatchType' the extra_data field "serverlessActionTypes" is used + # by the AuthorizationSession while in multitenancy mode to determine + # whether or not an action is reasonable to be performed by a user + # who has been authorized via security token. + # See: MatchType: - description: "test" + description: Resource Match Types used in describing privilege grants. type: string values: - # Matches no resource. - kMatchNever : "never" - # Matches if the resource is the cluster resource. - kMatchClusterResource : "cluster" - # Matches if the resource's database name is _ns.db(). - kMatchDatabaseName : "database" - # Matches if the resource's collection name is _ns.coll(). - kMatchCollectionName : "collection" - # Matches if the resource's namespace name is _ns. - kMatchExactNamespace : "exact_namespace" - # Matches all databases and non-system collections. - kMatchAnyNormalResource : "any_normal" - # Matches absolutely anything. - kMatchAnyResource : "any" - # Matches a collection named "<db>.system.buckets.<collection>" - kMatchExactSystemBucketResource : "system_buckets" - # Matches a collection named "system.buckets.<collection>" in any db - kMatchSystemBucketInAnyDBResource : "system_buckets_in_any_db" - # Matches any collection with a prefix of "system.buckets." in db - kMatchAnySystemBucketInDBResource : "any_system_buckets_in_db" - # Matches any collection with a prefix of "system.buckets." in any db - kMatchAnySystemBucketResource : "any_system_buckets" + kMatchNever: + description: Bottom type for resource matches, matches nothing. + value: "never" + extra_data: + serverlessActionTypes: [] # Explicitly listing no action types valid. + + # resource: { cluster: true } + kMatchClusterResource: + description: Matches if the resource is the cluster resource. + value: "cluster" + extra_data: + serverlessActionTypes: + - killAnyCursor + - killAnySession + - killCursors + - killop + - listDatabases + + # resource: { db: '', collection: 'exact' } + kMatchCollectionName: + description: Matches if the resource's collection is a particular name. + value: "collection" + extra_data: + serverlessActionTypes: &actionsValidOnCollection + - bypassDocumentValidation + - changeStream + - collMod + - collStats + - convertToCapped + - createCollection + - createIndex + - dbCheck + - dbHash + - dbStats + - dropCollection + - dropIndex + - exportCollection + - find + - importCollection + - insert + - killAnyCursor + - killCursors + - listCollections + - listIndexes + - planCacheRead + - reIndex + - remove + - renameCollection + - renameCollectionSameDB + - update + - validate + + # resource: { db: 'exact', collection: '' } + kMatchDatabaseName: + description: Matches if the resource's database is a particular name. + value: "database" + extra_data: + serverlessActionTypes: &actionsValidOnDatabase + # Actions common to collection patterns. + # YAML doesn't support extending list aliases. + # Make changes above, then copy here. + - bypassDocumentValidation + - changeStream + - collMod + - collStats + - convertToCapped + - createCollection + - createIndex + - dbCheck + - dbHash + - dbStats + - dropCollection + - dropIndex + - exportCollection + - find + - importCollection + - insert + - killAnyCursor + - killCursors + - listCollections + - listIndexes + - planCacheRead + - reIndex + - remove + - renameCollection + - renameCollectionSameDB + - update + - validate + + # Actions specific to the database match types. + - applicationMessage + - dropDatabase + - viewRole + - viewUser + + # resource: { db: 'exact', collection: 'exact' } + kMatchExactNamespace: + description: Matches if the resource is an exact namespace. + value: "exact_namespace" + extra_data: + serverlessActionTypes: *actionsValidOnCollection + + # resource: { db: '', collection: '' } + kMatchAnyNormalResource: + description: Matches all databases and non-system collections. + value: "any_normal" + extra_data: + serverlessActionTypes: *actionsValidOnDatabase + + # resource: { anyResource: true } + kMatchAnyResource: + description: Matches absolutely anything. + value: "any" + extra_data: + serverlessActionTypes: *actionsValidOnDatabase + + # resource: { db: 'exact', system_buckets: 'exact' } + kMatchExactSystemBucketResource: + description: Matches a collection named "<db>.system.buckets.<collection>" + value: "system_buckets" + extra_data: + serverlessActionTypes: *actionsValidOnCollection + + # resource: { db: '', system_buckets: 'exact' } + kMatchSystemBucketInAnyDBResource: + description: Matches a collection named "system.buckets.<collection>" in any db + value: "system_buckets_in_any_db" + extra_data: + serverlessActionTypes: *actionsValidOnCollection + + # resource: { db: 'exact', system_buckets: '' } + kMatchAnySystemBucketInDBResource: + description: Matches any collection with a prefix of "system.buckets." in a specific db + value: "any_system_buckets_in_db" + extra_data: + serverlessActionTypes: *actionsValidOnCollection + + # resource: { db: '', system_buckets: '' } + kMatchAnySystemBucketResource: + description: Matches any collection with a prefix of "system.buckets." in any db + value: "any_system_buckets" + extra_data: + serverlessActionTypes: *actionsValidOnCollection + +structs: + MatchTypeExtraData: + description: Extra data defined in the MatchType enum + fields: + serverlessActionTypes: + description: Permitted action types for the match type when in serverless mode + type: array<string> diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp index be2ad897b27..e8b2f908556 100644 --- a/src/mongo/db/auth/authorization_session_impl.cpp +++ b/src/mongo/db/auth/authorization_session_impl.cpp @@ -83,6 +83,73 @@ bool checkContracts() { return true; } +using ServerlessPermissionMap = stdx::unordered_map<MatchTypeEnum, ActionSet>; +ServerlessPermissionMap kServerlessPrivilegesPermitted; + +/** + * Load extra data from action_types.idl into runtime structure. + * For any given resource match type, we allow only the ActionTypes named + * to be granted to security token based users. + */ +MONGO_INITIALIZER(ServerlessPrivilegePermittedMap)(InitializerContext*) try { + ServerlessPermissionMap ret; + + for (std::size_t i = 0; i < kNumMatchTypeEnum; ++i) { + auto matchType = static_cast<MatchTypeEnum>(i); + auto matchTypeName = MatchType_serializer(matchType); + auto dataObj = MatchType_get_extra_data(matchType); + auto data = MatchTypeExtraData::parse({matchTypeName}, dataObj); + auto actionTypes = data.getServerlessActionTypes(); + + std::vector<std::string> actionsToParse; + std::transform(actionTypes.cbegin(), + actionTypes.cend(), + std::back_inserter(actionsToParse), + [](const auto& at) { return at.toString(); }); + + ActionSet actions; + std::vector<std::string> unknownActions; + auto status = + ActionSet::parseActionSetFromStringVector(actionsToParse, &actions, &unknownActions); + if (!status.isOK()) { + StringBuilder sb; + sb << "Unknown actions listed for match type '" << matchTypeName << "':"; + for (const auto& unknownAction : unknownActions) { + sb << " '" << unknownAction << "'"; + } + uassertStatusOK(status.withContext(sb.str())); + } + + ret[matchType] = std::move(actions); + } + + kServerlessPrivilegesPermitted = std::move(ret); +} catch (const DBException& ex) { + uassertStatusOK(ex.toStatus().withContext("Failed parsing extraData for MatchType enum")); +} + +void validateSecurityTokenUserPrivileges(const User::ResourcePrivilegeMap& privs) { + for (const auto& priv : privs) { + auto matchType = priv.first.matchType(); + const auto& actions = priv.second.getActions(); + auto it = kServerlessPrivilegesPermitted.find(matchType); + // This actually can't happen since the initializer above populated the map with all match + // types. + uassert(6161701, + str::stream() << "Unknown matchType: " << MatchType_serializer(matchType), + it != kServerlessPrivilegesPermitted.end()); + if (MONGO_unlikely(!it->second.isSupersetOf(actions))) { + auto unauthorized = actions; + unauthorized.removeAllActionsFromSet(it->second); + uasserted(6161702, + str::stream() + << "Security Token user has one or more actions not approved for " + "resource matchType '" + << MatchType_serializer(matchType) << "': " << unauthorized.toString()); + } + } +} + MONGO_FAIL_POINT_DEFINE(allowMultipleUsersWithApiStrict); } // namespace @@ -127,7 +194,7 @@ void AuthorizationSessionImpl::startContractTracking() { } Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, - const UserName& userName) { + const UserName& userName) try { auto checkForMultipleUsers = [&]() { const auto userCount = _authenticatedUsers.count(); if (userCount == 0) { @@ -201,6 +268,7 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, uassert(6161502, "Attempt to authorize a user other than that present in the security token", token->getAuthenticatedUser() == userName); + validateSecurityTokenUserPrivileges(user->getPrivileges()); _authenticationMode = AuthenticationMode::kSecurityToken; } else { _authenticationMode = AuthenticationMode::kConnection; @@ -212,6 +280,9 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, _buildAuthenticatedRolesVector(); return Status::OK(); + +} catch (const DBException& ex) { + return ex.toStatus(); } User* AuthorizationSessionImpl::lookupUser(const UserName& name) { diff --git a/src/mongo/db/auth/resource_pattern.h b/src/mongo/db/auth/resource_pattern.h index e8678eab848..5b4a952b886 100644 --- a/src/mongo/db/auth/resource_pattern.h +++ b/src/mongo/db/auth/resource_pattern.h @@ -261,7 +261,6 @@ public: return ResourcePattern(e); } -private: // AuthorizationContract works directly with MatchTypeEnum. Users should not be concerned with // how a ResourcePattern was constructed. MatchTypeEnum matchType() const { |