summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSara Golemon <sara.golemon@mongodb.com>2021-12-13 18:32:55 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-12-29 17:43:13 +0000
commitfd163ac958a6263a89b55faed127eceff9fea2f0 (patch)
tree1df62ed2676f33eec8e9305fbedecd5aa51bb9a0
parenta8305a25b2b5627e42c180279a870c50780df28b (diff)
downloadmongo-fd163ac958a6263a89b55faed127eceff9fea2f0.tar.gz
SERVER-61617 Restrict actionType/matchType combinations on serverless
-rw-r--r--buildscripts/idl/idl/enum_types.py4
-rw-r--r--jstests/auth/token_privileges.js80
-rw-r--r--src/mongo/db/auth/action_type.idl183
-rw-r--r--src/mongo/db/auth/authorization_session_impl.cpp73
-rw-r--r--src/mongo/db/auth/resource_pattern.h1
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 {