diff options
author | Pol Pinol Castuera <pol.castuera@mongodb.com> | 2022-09-12 11:04:41 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-09-12 12:04:02 +0000 |
commit | df26c0510bc089dd3c5a6f452249bde4b186a9b7 (patch) | |
tree | 95d17e88a22940273d7ad48e7d2fbadfac461811 | |
parent | b1d33c08186ae0d6c8dae7f9531c652c2ece1730 (diff) | |
download | mongo-df26c0510bc089dd3c5a6f452249bde4b186a9b7.tar.gz |
SERVER-68249 Add required privileges on the $_internalAllCollectionStats aggregation stage
9 files changed, 213 insertions, 17 deletions
diff --git a/jstests/sharding/all_collection_stats_auth.js b/jstests/sharding/all_collection_stats_auth.js new file mode 100644 index 00000000000..caafe005631 --- /dev/null +++ b/jstests/sharding/all_collection_stats_auth.js @@ -0,0 +1,91 @@ +/* + * Test to validate the privileges of using $_internalAllCollectionStats stage. + * + * @tags: [ + * requires_fcv_62, + * ] + */ + +(function() { +'use strict'; + +if (!TestData.auth) { + jsTestLog("Skipping testing authorization since auth is not enabled"); + return; +} + +// Test privileges +function testPrivileges() { + // Create new role with the exact privileges to execute $allCollectionStats + assert.commandWorked(adminDb.runCommand({ + createRole: "role_ok_priv", + roles: [], + privileges: [{resource: {cluster: true}, actions: ["allCollectionStats"]}] + })); + + // Creates users with privileges and no privileges + assert.commandWorked(adminDb.runCommand({createUser: "user_no_priv", pwd: "pwd", roles: []})); + + assert.commandWorked(adminDb.runCommand( + {createUser: "user_priv1", pwd: "pwd", roles: [{role: "role_ok_priv", db: 'admin'}]})); + + assert.commandWorked(adminDb.runCommand( + {createUser: "user_priv2", pwd: "pwd", roles: [{role: "clusterMonitor", db: 'admin'}]})); + + assert(adminDb.logout()); + + // User is in a role with privileges to execute the stage + assert(adminDb.auth("user_priv1", "pwd")); + assert.commandWorked(adminDb.runCommand({ + aggregate: 1, + pipeline: [{$_internalAllCollectionStats: {stats: {storageStats: {}}}}], + cursor: {} + })); + assert(adminDb.logout()); + + // User is in a role with privileges to execute the stage + assert(adminDb.auth("user_priv2", "pwd")); + assert.commandWorked(adminDb.runCommand({ + aggregate: 1, + pipeline: [{$_internalAllCollectionStats: {stats: {storageStats: {}}}}], + cursor: {} + })); + assert(adminDb.logout()); + + // User has no privileges to execute the stage + assert(adminDb.auth("user_no_priv", "pwd")); + assert.commandFailedWithCode( + adminDb.runCommand({ + aggregate: 1, + pipeline: [{$_internalAllCollectionStats: {stats: {storageStats: {}}}}], + cursor: {} + }), + ErrorCodes.Unauthorized, + "user should no longer have privileges to execute $_internalAllCollectionStats stage."); + assert(adminDb.logout()); +} + +// Configure initial sharding cluster +const st = new ShardingTest({shards: 1}); +const mongos = st.s; + +const ns1 = "test.foo"; +const adminDb = mongos.getDB("admin"); +const testDb = mongos.getDB("test"); + +// Create a super user with __system role. +assert.commandWorked(adminDb.runCommand({createUser: "super", pwd: "super", roles: ["__system"]})); +assert(adminDb.logout()); +assert(adminDb.auth("super", "super")); + +st.adminCommand({shardcollection: ns1, key: {skey: 1}}); + +// Insert data to validate the aggregation stage +for (let i = 0; i < 6; i++) { + assert.commandWorked(testDb.getCollection("foo").insert({skey: i})); +} + +testPrivileges(); + +st.stop(); +})(); diff --git a/jstests/sharding/sharded_data_distribution_auth.js b/jstests/sharding/sharded_data_distribution_auth.js new file mode 100644 index 00000000000..5d1318bc287 --- /dev/null +++ b/jstests/sharding/sharded_data_distribution_auth.js @@ -0,0 +1,81 @@ +/* + * Test to validate the privileges of using $shardedDataDistribution stage. + * + * @tags: [ + * requires_fcv_62, + * ] + */ + +(function() { +'use strict'; + +if (!TestData.auth) { + jsTestLog("Skipping testing authorization since auth is not enabled"); + return; +} + +// Test privileges +function testPrivileges() { + // Create new role with the exact privileges to execute $shardedDataDistribution + assert.commandWorked(adminDb.runCommand({ + createRole: "role_ok_priv", + roles: [], + privileges: [{resource: {cluster: true}, actions: ["shardedDataDistribution"]}] + })); + + // Creates users with privileges and no privileges + assert.commandWorked(adminDb.runCommand({createUser: "user_no_priv", pwd: "pwd", roles: []})); + + assert.commandWorked(adminDb.runCommand( + {createUser: "user_priv1", pwd: "pwd", roles: [{role: "role_ok_priv", db: 'admin'}]})); + + assert.commandWorked(adminDb.runCommand( + {createUser: "user_priv2", pwd: "pwd", roles: [{role: "clusterMonitor", db: 'admin'}]})); + + assert(adminDb.logout()); + + // User is in a role with privileges to execute the stage + assert(adminDb.auth("user_priv1", "pwd")); + assert.commandWorked( + adminDb.runCommand({aggregate: 1, pipeline: [{$shardedDataDistribution: {}}], cursor: {}})); + assert(adminDb.logout()); + + // User is in a role with privileges to execute the stage + assert(adminDb.auth("user_priv2", "pwd")); + assert.commandWorked( + adminDb.runCommand({aggregate: 1, pipeline: [{$shardedDataDistribution: {}}], cursor: {}})); + assert(adminDb.logout()); + + // User has no privileges to execute the stage + assert(adminDb.auth("user_no_priv", "pwd")); + assert.commandFailedWithCode( + adminDb.runCommand({aggregate: 1, pipeline: [{$shardedDataDistribution: {}}], cursor: {}}), + ErrorCodes.Unauthorized, + "user should no longer have privileges to execute $shardedDataDistribution stage."); + assert(adminDb.logout()); +} + +// Configure initial sharding cluster +const st = new ShardingTest({shards: 1}); +const mongos = st.s; + +const ns1 = "test.foo"; +const adminDb = mongos.getDB("admin"); +const testDb = mongos.getDB("test"); + +// Create a super user with __system role. +assert.commandWorked(adminDb.runCommand({createUser: "super", pwd: "super", roles: ["__system"]})); +assert(adminDb.logout()); +assert(adminDb.auth("super", "super")); + +st.adminCommand({shardcollection: ns1, key: {skey: 1}}); + +// Insert data to validate the aggregation stage +for (let i = 0; i < 6; i++) { + assert.commandWorked(testDb.getCollection("foo").insert({skey: i})); +} + +testPrivileges(); + +st.stop(); +})(); diff --git a/src/mongo/db/auth/action_type.idl b/src/mongo/db/auth/action_type.idl index dbc5fe12efb..0d1172330eb 100644 --- a/src/mongo/db/auth/action_type.idl +++ b/src/mongo/db/auth/action_type.idl @@ -45,6 +45,7 @@ enums: values: addShard : "addShard" advanceClusterTime : "advanceClusterTime" + allCollectionStats: "allCollectionStats" analyze : "analyze" anyAction : "anyAction" # Special ActionType that represents *all* actions appendOplogNote : "appendOplogNote" @@ -168,6 +169,7 @@ enums: setParameter : "setParameter" setUserWriteBlockMode: "setUserWriteBlockMode" shardCollection : "shardCollection" # ID only + shardedDataDistribution : "shardedDataDistribution" shardingState : "shardingState" shutdown : "shutdown" splitChunk : "splitChunk" diff --git a/src/mongo/db/auth/builtin_roles.yml b/src/mongo/db/auth/builtin_roles.yml index d0634f8a9f7..43401882084 100644 --- a/src/mongo/db/auth/builtin_roles.yml +++ b/src/mongo/db/auth/builtin_roles.yml @@ -244,6 +244,8 @@ roles: - useUUID - inprog - shardingState + - allCollectionStats + - shardedDataDistribution - matchType: any_normal actions: &clusterMonitorRoleDatabaseActions - collStats diff --git a/src/mongo/db/pipeline/aggregate_command.idl b/src/mongo/db/pipeline/aggregate_command.idl index 7cc4840f273..1e772a3bb2d 100644 --- a/src/mongo/db/pipeline/aggregate_command.idl +++ b/src/mongo/db/pipeline/aggregate_command.idl @@ -156,6 +156,14 @@ commands: - privilege: # $backupCursorExtend, backupCursor resource_pattern: cluster action_type: fsync + - privilege: # $_internalAllCollectionStats + agg_stage: _internalAllCollectionStats + resource_pattern: cluster + action_type: allCollectionStats + - privilege: # $shardedDataDistribution + agg_stage: shardedDataDistribution + resource_pattern: cluster + action_type: shardedDataDistribution # Note that the 'CursorInitialReply' is not the only response that an aggregate command # could return. With 'explain' or 'exchange', the response would not include the fields in # 'CursorInitialReply'. But using 'explain' or 'exchange' is unstable, but otherwise the diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp index e4ff28d9fcf..3e22cc826e9 100644 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp +++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp @@ -43,15 +43,7 @@ DocumentSourceInternalAllCollectionStats::DocumentSourceInternalAllCollectionSta REGISTER_DOCUMENT_SOURCE(_internalAllCollectionStats, DocumentSourceInternalAllCollectionStats::LiteParsed::parse, DocumentSourceInternalAllCollectionStats::createFromBsonInternal, - AllowedWithApiStrict::kAlways); - -PrivilegeVector DocumentSourceInternalAllCollectionStats::LiteParsed::requiredPrivileges( - bool isMongos, bool bypassDocumentValidation) const { - - // TODO: SERVER-68249 - - return PrivilegeVector{Privilege(ResourcePattern::forAnyNormalResource(), ActionType::find)}; -} + AllowedWithApiStrict::kInternal); DocumentSource::GetNextResult DocumentSourceInternalAllCollectionStats::doGetNext() { if (!_catalogDocs) { diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h index 87217a86ab8..f3c89bbf93a 100644 --- a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h +++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h @@ -68,7 +68,10 @@ public: } PrivilegeVector requiredPrivileges(bool isMongos, - bool bypassDocumentValidation) const final; + bool bypassDocumentValidation) const final { + return { + Privilege(ResourcePattern::forClusterResource(), ActionType::allCollectionStats)}; + } bool isInitialSource() const final { return true; diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp index 61cc3e44bba..8edacb97b10 100644 --- a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp +++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp @@ -46,7 +46,7 @@ using boost::intrusive_ptr; using std::list; REGISTER_DOCUMENT_SOURCE(shardedDataDistribution, - LiteParsedDocumentSourceDefault::parse, + DocumentSourceShardedDataDistribution::LiteParsed::parse, DocumentSourceShardedDataDistribution::createFromBson, AllowedWithApiStrict::kAlways); @@ -63,12 +63,6 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceShardedDataDistribution::creat "The $shardedDataDistribution stage must be run on the admin database", expCtx->ns.isAdminDB() && expCtx->ns.isCollectionlessAggregateNS()); - // Add 'config.collections' as a resolved namespace - StringMap<ExpressionContext::ResolvedNamespace> resolvedNamespaces; - resolvedNamespaces[NamespaceString::kConfigsvrCollectionsNamespace.coll()] = { - NamespaceString::kConfigsvrCollectionsNamespace, std::vector<BSONObj>{}}; - expCtx->setResolvedNamespaces(resolvedNamespaces); - static const BSONObj kAllCollStatsObj = fromjson("{$_internalAllCollectionStats: {stats: {storageStats: {}}}}}"); static const BSONObj kGroupObj = fromjson(R"({ diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.h b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h index 7cf96c85972..23705cf6aeb 100644 --- a/src/mongo/db/pipeline/document_source_sharded_data_distribution.h +++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h @@ -42,6 +42,29 @@ namespace DocumentSourceShardedDataDistribution { static constexpr StringData kStageName = "$shardedDataDistribution"_sd; +class LiteParsed final : public LiteParsedDocumentSource { +public: + static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, const BSONElement& spec) { + return std::make_unique<LiteParsed>(spec.fieldName()); + } + + explicit LiteParsed(std::string parseTimeName) + : LiteParsedDocumentSource(std::move(parseTimeName)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return {NamespaceString::kConfigsvrCollectionsNamespace}; + } + + PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final { + return { + Privilege(ResourcePattern::forClusterResource(), ActionType::shardedDataDistribution)}; + } + + bool isInitialSource() const final { + return true; + } +}; + static std::list<boost::intrusive_ptr<DocumentSource>> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); |