summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough_auth.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/read_concern_linearizable_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_causally_consistent_jscore_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/sharding_jscore_op_query_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml2
-rw-r--r--jstests/auth/lib/commands_lib.js16
-rw-r--r--jstests/auth/list_all_local_sessions.js56
-rw-r--r--jstests/auth/list_local_sessions.js72
-rw-r--r--jstests/core/list_all_local_sessions.js25
-rw-r--r--jstests/core/list_local_sessions.js45
-rw-r--r--src/mongo/db/auth/action_types.txt1
-rw-r--r--src/mongo/db/auth/authorization_session.cpp4
-rw-r--r--src/mongo/db/auth/role_graph_builtin_roles.cpp2
-rw-r--r--src/mongo/db/logical_session_cache.h18
-rw-r--r--src/mongo/db/logical_session_cache_impl.cpp32
-rw-r--r--src/mongo/db/logical_session_cache_impl.h9
-rw-r--r--src/mongo/db/logical_session_cache_noop.h13
-rw-r--r--src/mongo/db/logical_session_id_helpers.cpp23
-rw-r--r--src/mongo/db/logical_session_id_helpers.h10
-rw-r--r--src/mongo/db/pipeline/SConscript2
-rw-r--r--src/mongo/db/pipeline/document_source_change_stream.h4
-rw-r--r--src/mongo/db/pipeline/document_source_list_local_sessions.cpp166
-rw-r--r--src/mongo/db/pipeline/document_source_list_local_sessions.h118
-rw-r--r--src/mongo/db/pipeline/document_sources.idl18
-rw-r--r--src/mongo/db/pipeline/lite_parsed_document_source.h7
-rw-r--r--src/mongo/db/pipeline/lite_parsed_pipeline.h9
-rw-r--r--src/mongo/s/commands/cluster_aggregate.cpp6
30 files changed, 658 insertions, 14 deletions
diff --git a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough.yml
index 637b6dba2b3..3b3a9832ea5 100644
--- a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough.yml
@@ -168,6 +168,8 @@ selector:
- jstests/core/views/views_find.js
# TODO: SERVER-30582
- jstests/core/explain_multi_plan.js
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
executor:
config:
diff --git a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough_auth.yml b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough_auth.yml
index ed867dfc062..e1fb56efc39 100644
--- a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough_auth.yml
+++ b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_passthrough_auth.yml
@@ -182,6 +182,8 @@ selector:
- jstests/core/views/views_find.js
# TODO: SERVER-30582
- jstests/core/explain_multi_plan.js
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
executor:
config:
diff --git a/buildscripts/resmokeconfig/suites/read_concern_linearizable_passthrough.yml b/buildscripts/resmokeconfig/suites/read_concern_linearizable_passthrough.yml
index bb9b3c0b6b8..ae5845f7724 100644
--- a/buildscripts/resmokeconfig/suites/read_concern_linearizable_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/read_concern_linearizable_passthrough.yml
@@ -73,6 +73,8 @@ selector:
# Comments list possible problem point under review.
- jstests/core/capped6.js # Uses captrunc test command.
- jstests/core/stages_delete.js # Uses stageDebug command for deletes.
+ - jstests/core/list_all_local_sessions.js # collectionless aggregation stage
+ - jstests/core/list_local_sessions.js # collectionless aggregation stage
executor:
config:
diff --git a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
index 0950065ba2e..73bfa5a27b9 100644
--- a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
@@ -70,6 +70,8 @@ selector:
# Comments list possible problem point under review.
- jstests/core/capped6.js # Uses captrunc test command.
- jstests/core/stages_delete.js # Uses stageDebug command for deletes.
+ - jstests/core/list_all_local_sessions.js # collectionless aggregation stage
+ - jstests/core/list_local_sessions.js # collectionless aggregation stage
executor:
config:
diff --git a/buildscripts/resmokeconfig/suites/sharded_causally_consistent_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharded_causally_consistent_jscore_passthrough.yml
index a655a414298..2f3e11b8a15 100644
--- a/buildscripts/resmokeconfig/suites/sharded_causally_consistent_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharded_causally_consistent_jscore_passthrough.yml
@@ -187,6 +187,8 @@ selector:
- jstests/core/explain_shell_helpers.js
- jstests/core/sort_array.js
- jstests/core/explain_multi_plan.js
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
exclude_with_any_tags:
# Tests tagged with the following will fail because they assume collections are not sharded.
- assumes_no_implicit_collection_creation_after_drop
diff --git a/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
index 708104e1b04..38405f89481 100644
--- a/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
@@ -76,6 +76,8 @@ selector:
- jstests/core/mr4.js
# TODO: SERVER-30052
- jstests/core/queryoptimizer3.js
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
exclude_with_any_tags:
# Tests tagged with the following will fail because they assume collections are not sharded.
- assumes_no_implicit_collection_creation_after_drop
diff --git a/buildscripts/resmokeconfig/suites/sharding_jscore_op_query_passthrough.yml b/buildscripts/resmokeconfig/suites/sharding_jscore_op_query_passthrough.yml
index 5c60ccbf1b5..298476e37ef 100644
--- a/buildscripts/resmokeconfig/suites/sharding_jscore_op_query_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharding_jscore_op_query_passthrough.yml
@@ -48,6 +48,8 @@ selector:
# TODO: SERVER-27269: mongos can't establish cursor if view has $collStats and views another view.
- jstests/core/views/views_coll_stats.js
- jstests/core/killop_drop_collection.js # Uses fsyncLock.
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
executor:
diff --git a/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
index ebecf412b87..b6b3dde7e5b 100644
--- a/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
@@ -48,6 +48,8 @@ selector:
# TODO: SERVER-27269: mongos can't establish cursor if view has $collStats and views another view.
- jstests/core/views/views_coll_stats.js
- jstests/core/killop_drop_collection.js # Uses fsyncLock.
+ - jstests/core/list_local_sessions.js # SERVER-29628 needs MongoSOnly support
+ - jstests/core/list_all_local_sessions.js # SERVER-29628 needs MongoSOnly support
executor:
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js
index 10cde2c2382..db35708e96c 100644
--- a/jstests/auth/lib/commands_lib.js
+++ b/jstests/auth/lib/commands_lib.js
@@ -661,6 +661,22 @@ var authCommandsLib = {
skipSharded: true
},
{
+ testname: "aggregate_listLocalSessions_allUsers_true",
+ command: {aggregate: 1, pipeline: [{$listLocalSessions: {allUsers: true}}], cursor: {}},
+ testcases: [{
+ runOnDb: adminDbName,
+ roles:
+ {clusterAdmin: 1, clusterMonitor: 1, clusterManager: 1, root: 1, __system: 1}
+ }],
+ skipSharded: true
+ },
+ {
+ testname: "aggregate_listLocalSessions_allUsers_false",
+ command: {aggregate: 1, pipeline: [{$listLocalSessions: {allUsers: false}}], cursor: {}},
+ testcases: [{runOnDb: adminDbName, roles: roles_all}],
+ skipSharded: true
+ },
+ {
testname: "aggregate_lookup",
command: {
aggregate: "foo",
diff --git a/jstests/auth/list_all_local_sessions.js b/jstests/auth/list_all_local_sessions.js
new file mode 100644
index 00000000000..2d67011bf06
--- /dev/null
+++ b/jstests/auth/list_all_local_sessions.js
@@ -0,0 +1,56 @@
+// Auth tests for the $listLocalSessions {allUsers:true} aggregation stage.
+
+(function() {
+ 'use strict';
+ load('jstests/aggregation/extras/utils.js');
+
+ function runListAllLocalSessionsTest(mongod) {
+ assert(mongod);
+ const admin = mongod.getDB("admin");
+ const db = mongod.getDB("test");
+
+ const pipeline = [{'$listLocalSessions': {allUsers: true}}];
+ function listAllLocalSessions() {
+ return admin.aggregate(pipeline);
+ }
+
+ admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
+ assert(admin.auth('admin', 'pass'));
+ db.createUser({user: 'user1', pwd: 'pass', roles: jsTest.basicUserRoles});
+ admin.logout();
+
+ // Shouldn't be able to listLocalSessions when not logged in.
+ assertErrorCode(admin, pipeline, ErrorCodes.Unauthorized);
+
+ // Start a new session and capture its sessionId.
+ assert(db.auth('user1', 'pass'));
+ const myid = assert.commandWorked(db.runCommand({startSession: 1})).id.id;
+ assert(myid !== undefined);
+
+ // Ensure that a normal user can NOT listAllLocalSessions to view their session.
+ assertErrorCode(admin, pipeline, ErrorCodes.Unauthorized);
+ db.logout();
+
+ // Ensure that the cache now contains the session and is visible by admin.
+ assert(admin.auth('admin', 'pass'));
+ const resultArray = assert.doesNotThrow(listAllLocalSessions).toArray();
+ assert.eq(resultArray.length, 1);
+ const cacheid = resultArray[0]._id.id;
+ assert(cacheid !== undefined);
+ assert.eq(0, bsonWoCompare({x: cacheid}, {x: myid}));
+ }
+
+ const mongod = MongoRunner.runMongod({auth: ""});
+ runListAllLocalSessionsTest(mongod);
+ MongoRunner.stopMongod(mongod);
+
+ if (true) {
+ // TODO: SERVER-29141 Enable mongos-only pipelines
+ return;
+ }
+
+ const st =
+ new ShardingTest({shards: 1, mongos: 1, config: 1, other: {keyFile: 'jstests/libs/key1'}});
+ runListAllLocalSessionsTest(st.s0);
+ st.stop();
+})();
diff --git a/jstests/auth/list_local_sessions.js b/jstests/auth/list_local_sessions.js
new file mode 100644
index 00000000000..c214b964d42
--- /dev/null
+++ b/jstests/auth/list_local_sessions.js
@@ -0,0 +1,72 @@
+// All tests for the $listLocalSessions aggregateion stage.
+
+(function() {
+ 'use strict';
+ load('jstests/aggregation/extras/utils.js');
+
+ function runListLocalSessionsTest(mongod) {
+ assert(mongod);
+ const admin = mongod.getDB('admin');
+ const db = mongod.getDB("test");
+
+ const pipeline = [{'$listLocalSessions': {}}];
+ function listLocalSessions() {
+ return admin.aggregate(pipeline);
+ }
+
+ admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
+ assert(admin.auth('admin', 'pass'));
+
+ db.createUser({user: 'user1', pwd: 'pass', roles: jsTest.basicUserRoles});
+ db.createUser({user: 'user2', pwd: 'pass', roles: jsTest.basicUserRoles});
+ admin.logout();
+
+ // Shouldn't be able to listLocalSessions when not logged in.
+ assertErrorCode(admin, pipeline, ErrorCodes.Unauthorized);
+
+ // Start a new session and capture its sessionId.
+ assert(db.auth('user1', 'pass'));
+ const myid = assert.commandWorked(db.runCommand({startSession: 1})).id.id;
+ assert(myid !== undefined);
+
+ // Ensure that the cache now contains the session.
+ const resultArray = assert.doesNotThrow(listLocalSessions).toArray();
+ assert.eq(resultArray.length, 1);
+ const cacheid = resultArray[0]._id.id;
+ const myuid = resultArray[0]._id.uid;
+ assert(cacheid !== undefined);
+ assert.eq(0, bsonWoCompare({x: cacheid}, {x: myid}));
+
+ // Try asking for the session by username.
+ function listMyLocalSessions() {
+ return admin.aggregate(
+ [{'$listLocalSessions': {users: [{user: "user1", db: "test"}]}}]);
+ }
+ const resultArrayMine = assert.doesNotThrow(listMyLocalSessions).toArray();
+ assert.eq(bsonWoCompare(resultArray, resultArrayMine), 0);
+
+ // Ensure that changing users hides the session.
+ assert(db.auth('user2', 'pass'));
+ const otherArray = assert.doesNotThrow(listLocalSessions).toArray();
+ assert.eq(otherArray.length, 0);
+
+ // Ensure that one user can not explicitly ask for another's sessions.
+ assertErrorCode(admin,
+ [{'$listLocalSessions': {users: [{user: "user1", db: "test"}]}}],
+ ErrorCodes.Unauthorized);
+ }
+
+ const mongod = MongoRunner.runMongod({auth: ""});
+ runListLocalSessionsTest(mongod);
+ MongoRunner.stopMongod(mongod);
+
+ if (true) {
+ // TODO SERVER-29141: Support forcing pipelines to run on mongos
+ return;
+ }
+
+ const st =
+ new ShardingTest({shards: 1, mongos: 1, config: 1, other: {keyFile: 'jstests/libs/key1'}});
+ runListLocalSessionsTest(st.s0);
+ st.stop();
+})();
diff --git a/jstests/core/list_all_local_sessions.js b/jstests/core/list_all_local_sessions.js
new file mode 100644
index 00000000000..9223773afbb
--- /dev/null
+++ b/jstests/core/list_all_local_sessions.js
@@ -0,0 +1,25 @@
+// Basic tests for the $listLocalSessions {allUsers: true} aggregation stage.
+
+(function() {
+ 'use strict';
+
+ const listAllLocalSessions = function() {
+ return db.aggregate([{'$listLocalSessions': {allUsers: true}}]);
+ };
+
+ // Start a new session and capture its sessionId.
+ const myid = assert.commandWorked(db.runCommand({startSession: 1})).id.id;
+ assert(myid !== undefined);
+
+ // Ensure that the cache now contains the session and is visible by admin.
+ const resultArray = assert.doesNotThrow(listAllLocalSessions).toArray();
+ assert.gte(resultArray.length, 1);
+ const resultArrayMine = resultArray
+ .map(function(sess) {
+ return sess._id.id;
+ })
+ .filter(function(id) {
+ return 0 == bsonWoCompare({x: id}, {x: myid});
+ });
+ assert.eq(resultArrayMine.length, 1);
+})();
diff --git a/jstests/core/list_local_sessions.js b/jstests/core/list_local_sessions.js
new file mode 100644
index 00000000000..14758fdb114
--- /dev/null
+++ b/jstests/core/list_local_sessions.js
@@ -0,0 +1,45 @@
+// Basic tests for the $listLocalSessions aggregation stage.
+
+(function() {
+ 'use strict';
+
+ function listLocalSessions() {
+ return db.aggregate([{'$listLocalSessions': {allUsers: false}}]);
+ }
+
+ // Start a new session and capture its sessionId.
+ const myid = assert.commandWorked(db.runCommand({startSession: 1})).id.id;
+ assert(myid !== undefined);
+
+ // Ensure that the cache now contains the session and is visible.
+ const resultArray = assert.doesNotThrow(listLocalSessions).toArray();
+ assert.gte(resultArray.length, 1);
+ const resultArrayMine = resultArray
+ .map(function(sess) {
+ return sess._id.id;
+ })
+ .filter(function(id) {
+ return 0 == bsonWoCompare({x: id}, {x: myid});
+ });
+ assert.eq(resultArrayMine.length, 1);
+
+ // Try asking for the session by username.
+ const myusername = (function() {
+ if (0 == bsonWoCompare({x: resultArray[0]._id.uid}, {x: computeSHA256Block("")})) {
+ // Code for "we're running in no-auth mode"
+ return {user: "", db: ""};
+ }
+ const connstats = assert.commandWorked(db.runCommand({connectionStatus: 1}));
+ const authUsers = connstats.authInfo.authenticatedUsers;
+ assert(authUsers !== undefined);
+ assert.eq(authUsers.length, 1);
+ assert(authUsers[0].user !== undefined);
+ assert(authUsers[0].db !== undefined);
+ return {user: authUsers[0].user, db: authUsers[0].db};
+ })();
+ function listMyLocalSessions() {
+ return db.aggregate([{'$listLocalSessions': {users: [myusername]}}]);
+ }
+ const myArray = assert.doesNotThrow(listMyLocalSessions).toArray();
+ assert.eq(myArray.length, resultArray.length);
+})();
diff --git a/src/mongo/db/auth/action_types.txt b/src/mongo/db/auth/action_types.txt
index d3b2745d319..06185ac9de9 100644
--- a/src/mongo/db/auth/action_types.txt
+++ b/src/mongo/db/auth/action_types.txt
@@ -70,6 +70,7 @@
"listCollections",
"listDatabases",
"listIndexes",
+"listSessions",
"listShards",
"logRotate",
"moveChunk",
diff --git a/src/mongo/db/auth/authorization_session.cpp b/src/mongo/db/auth/authorization_session.cpp
index 201acb11b00..86cba41f616 100644
--- a/src/mongo/db/auth/authorization_session.cpp
+++ b/src/mongo/db/auth/authorization_session.cpp
@@ -200,10 +200,10 @@ User* AuthorizationSession::getSingleUser() {
if (userNameItr.more()) {
userName = userNameItr.next();
if (userNameItr.more()) {
- uasserted(ErrorCodes::Unauthorized, "there are no users authenticated");
+ uasserted(ErrorCodes::Unauthorized, "too many users are authenticated");
}
} else {
- uasserted(ErrorCodes::Unauthorized, "too many users are authenticated");
+ uasserted(ErrorCodes::Unauthorized, "there are no users authenticated");
}
return lookupUser(userName);
diff --git a/src/mongo/db/auth/role_graph_builtin_roles.cpp b/src/mongo/db/auth/role_graph_builtin_roles.cpp
index 36cc6b4c15a..0ab6e26d052 100644
--- a/src/mongo/db/auth/role_graph_builtin_roles.cpp
+++ b/src/mongo/db/auth/role_graph_builtin_roles.cpp
@@ -186,6 +186,7 @@ MONGO_INITIALIZER(AuthorizationBuiltinRoles)(InitializerContext* context) {
<< ActionType::getShardMap
<< ActionType::hostInfo
<< ActionType::listDatabases
+ << ActionType::listSessions // clusterManager gets this also
<< ActionType::listShards // clusterManager gets this also
<< ActionType::netstat
<< ActionType::replSetGetConfig // clusterManager gets this also
@@ -238,6 +239,7 @@ MONGO_INITIALIZER(AuthorizationBuiltinRoles)(InitializerContext* context) {
<< ActionType::resync // hostManager gets this also
<< ActionType::addShard
<< ActionType::removeShard
+ << ActionType::listSessions // clusterMonitor gets this also
<< ActionType::listShards // clusterMonitor gets this also
<< ActionType::flushRouterConfig // hostManager gets this also
<< ActionType::cleanupOrphaned;
diff --git a/src/mongo/db/logical_session_cache.h b/src/mongo/db/logical_session_cache.h
index dfc1772013a..8e7ba166874 100644
--- a/src/mongo/db/logical_session_cache.h
+++ b/src/mongo/db/logical_session_cache.h
@@ -28,6 +28,8 @@
#pragma once
+#include <boost/optional.hpp>
+
#include "mongo/base/status.h"
#include "mongo/db/logical_session_id.h"
#include "mongo/db/refresh_sessions_gen.h"
@@ -103,6 +105,22 @@ public:
* Returns the number of session records currently in the cache.
*/
virtual size_t size() = 0;
+
+ /**
+ * Ennumerate all LogicalSessionId keys currently in the cache.
+ */
+ virtual std::vector<LogicalSessionId> listIds() const = 0;
+
+ /**
+ * Ennumerate all LogicalSessionId keys in the cache for the given UserDigests.
+ */
+ virtual std::vector<LogicalSessionId> listIds(
+ const std::vector<SHA256Block>& userDigest) const = 0;
+
+ /**
+ * Retrieve a LogicalSessionRecord by LogicalSessionId, if it exists in the cache.
+ */
+ virtual boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const = 0;
};
} // namespace mongo
diff --git a/src/mongo/db/logical_session_cache_impl.cpp b/src/mongo/db/logical_session_cache_impl.cpp
index c6dd6df3630..5cb38a51b08 100644
--- a/src/mongo/db/logical_session_cache_impl.cpp
+++ b/src/mongo/db/logical_session_cache_impl.cpp
@@ -263,4 +263,36 @@ boost::optional<LogicalSessionRecord> LogicalSessionCacheImpl::_addToCache(
return _cache.add(record.getId(), std::move(record));
}
+std::vector<LogicalSessionId> LogicalSessionCacheImpl::listIds() const {
+ stdx::lock_guard<stdx::mutex> lk(_cacheMutex);
+ std::vector<LogicalSessionId> ret;
+ ret.reserve(_cache.size());
+ for (const auto& id : _cache) {
+ ret.push_back(id.first);
+ }
+ return ret;
+}
+
+std::vector<LogicalSessionId> LogicalSessionCacheImpl::listIds(
+ const std::vector<SHA256Block>& userDigests) const {
+ stdx::lock_guard<stdx::mutex> lk(_cacheMutex);
+ std::vector<LogicalSessionId> ret;
+ for (const auto& it : _cache) {
+ if (std::find(userDigests.cbegin(), userDigests.cend(), it.first.getUid()) !=
+ userDigests.cend()) {
+ ret.push_back(it.first);
+ }
+ }
+ return ret;
+}
+
+boost::optional<LogicalSessionRecord> LogicalSessionCacheImpl::peekCached(
+ const LogicalSessionId& id) const {
+ stdx::lock_guard<stdx::mutex> lk(_cacheMutex);
+ const auto it = _cache.cfind(id);
+ if (it == _cache.cend()) {
+ return boost::none;
+ }
+ return it->second;
+}
} // namespace mongo
diff --git a/src/mongo/db/logical_session_cache_impl.h b/src/mongo/db/logical_session_cache_impl.h
index 92b8dd7301c..47a2f897f3b 100644
--- a/src/mongo/db/logical_session_cache_impl.h
+++ b/src/mongo/db/logical_session_cache_impl.h
@@ -123,6 +123,13 @@ public:
size_t size() override;
+ std::vector<LogicalSessionId> listIds() const override;
+
+ std::vector<LogicalSessionId> listIds(
+ const std::vector<SHA256Block>& userDigest) const override;
+
+ boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const override;
+
private:
/**
* Internal methods to handle scheduling and perform refreshes for active
@@ -147,7 +154,7 @@ private:
std::unique_ptr<ServiceLiason> _service;
std::unique_ptr<SessionsCollection> _sessionsColl;
- stdx::mutex _cacheMutex;
+ mutable stdx::mutex _cacheMutex;
LRUCache<LogicalSessionId, LogicalSessionRecord, LogicalSessionIdHash> _cache;
};
diff --git a/src/mongo/db/logical_session_cache_noop.h b/src/mongo/db/logical_session_cache_noop.h
index 59785ff240e..7c5e0be1784 100644
--- a/src/mongo/db/logical_session_cache_noop.h
+++ b/src/mongo/db/logical_session_cache_noop.h
@@ -73,6 +73,19 @@ public:
size_t size() override {
return 0;
}
+
+ std::vector<LogicalSessionId> listIds() const override {
+ return {};
+ }
+
+ std::vector<LogicalSessionId> listIds(
+ const std::vector<SHA256Block>& userDigest) const override {
+ return {};
+ }
+
+ boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const override {
+ return boost::none;
+ }
};
} // namespace mongo
diff --git a/src/mongo/db/logical_session_id_helpers.cpp b/src/mongo/db/logical_session_id_helpers.cpp
index 6e47690a111..e3de10a12b7 100644
--- a/src/mongo/db/logical_session_id_helpers.cpp
+++ b/src/mongo/db/logical_session_id_helpers.cpp
@@ -32,26 +32,26 @@
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/auth/user.h"
+#include "mongo/db/auth/user_name.h"
+#include "mongo/db/logical_session_cache.h"
#include "mongo/db/operation_context.h"
namespace mongo {
-namespace {
-
/**
* This is a safe hash that will not collide with a username because all full usernames include an
* '@' character.
*/
const auto kNoAuthDigest = SHA256Block::computeHash(reinterpret_cast<const uint8_t*>(""), 0);
-SHA256Block lookupUserDigest(OperationContext* opCtx) {
+SHA256Block getLogicalSessionUserDigestForLoggedInUser(const OperationContext* opCtx) {
auto client = opCtx->getClient();
ServiceContext* serviceContext = client->getServiceContext();
if (AuthorizationManager::get(serviceContext)->isAuthEnabled()) {
UserName userName;
- auto user = AuthorizationSession::get(client)->getSingleUser();
+ const auto user = AuthorizationSession::get(client)->getSingleUser();
invariant(user);
return user->getDigest();
@@ -60,7 +60,14 @@ SHA256Block lookupUserDigest(OperationContext* opCtx) {
}
}
-} // namespace
+SHA256Block getLogicalSessionUserDigestFor(StringData user, StringData db) {
+ if (user.empty() && db.empty()) {
+ return kNoAuthDigest;
+ }
+ const UserName un(user, db);
+ const auto& fn = un.getFullName();
+ return SHA256Block::computeHash({ConstDataRange(fn.c_str(), fn.size())});
+}
LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& fromClient,
OperationContext* opCtx,
@@ -81,11 +88,11 @@ LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& fromClient
}) ||
authSession->isAuthorizedForPrivilege(Privilege(
ResourcePattern::forClusterResource(), ActionType::impersonate)) ||
- lookupUserDigest(opCtx) == fromClient.getUid());
+ getLogicalSessionUserDigestForLoggedInUser(opCtx) == fromClient.getUid());
lsid.setUid(*fromClient.getUid());
} else {
- lsid.setUid(lookupUserDigest(opCtx));
+ lsid.setUid(getLogicalSessionUserDigestForLoggedInUser(opCtx));
}
return lsid;
@@ -95,7 +102,7 @@ LogicalSessionId makeLogicalSessionId(OperationContext* opCtx) {
LogicalSessionId id{};
id.setId(UUID::gen());
- id.setUid(lookupUserDigest(opCtx));
+ id.setUid(getLogicalSessionUserDigestForLoggedInUser(opCtx));
return id;
}
diff --git a/src/mongo/db/logical_session_id_helpers.h b/src/mongo/db/logical_session_id_helpers.h
index 2b4c2e76e61..a913ac3c769 100644
--- a/src/mongo/db/logical_session_id_helpers.h
+++ b/src/mongo/db/logical_session_id_helpers.h
@@ -37,6 +37,16 @@
namespace mongo {
/**
+ * Get the currently logged in user's UID digest.
+ */
+SHA256Block getLogicalSessionUserDigestForLoggedInUser(const OperationContext* opCtx);
+
+/**
+ * Get a user digest for a specific user/db identifier.
+ */
+SHA256Block getLogicalSessionUserDigestFor(StringData user, StringData db);
+
+/**
* Factory functions to generate logical session records.
*/
LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& lsid,
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index 1d622a2c8cf..ce3a9c4c43e 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -244,6 +244,7 @@ docSourceEnv.Library(
'document_source_internal_inhibit_optimization.cpp',
'document_source_internal_split_pipeline.cpp',
'document_source_limit.cpp',
+ 'document_source_list_local_sessions.cpp',
'document_source_match.cpp',
'document_source_merge_cursors.cpp',
'document_source_mock.cpp',
@@ -263,6 +264,7 @@ docSourceEnv.Library(
'$BUILD_DIR/mongo/client/clientdriver',
'$BUILD_DIR/mongo/db/bson/dotted_path_support',
'$BUILD_DIR/mongo/db/index/key_generator',
+ '$BUILD_DIR/mongo/db/logical_session_cache_impl',
'$BUILD_DIR/mongo/db/matcher/expression_algo',
'$BUILD_DIR/mongo/db/matcher/expressions',
'$BUILD_DIR/mongo/db/pipeline/lite_parsed_document_source',
diff --git a/src/mongo/db/pipeline/document_source_change_stream.h b/src/mongo/db/pipeline/document_source_change_stream.h
index 39aed452ed4..93dcd5483b8 100644
--- a/src/mongo/db/pipeline/document_source_change_stream.h
+++ b/src/mongo/db/pipeline/document_source_change_stream.h
@@ -61,6 +61,10 @@ public:
PrivilegeVector requiredPrivileges(bool isMongos) const final {
return {};
}
+
+ bool allowedToForwardFromMongos() const final {
+ return false;
+ }
};
class Transformation : public DocumentSourceSingleDocumentTransformation::TransformerInterface {
diff --git a/src/mongo/db/pipeline/document_source_list_local_sessions.cpp b/src/mongo/db/pipeline/document_source_list_local_sessions.cpp
new file mode 100644
index 00000000000..af4d454d208
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_list_local_sessions.cpp
@@ -0,0 +1,166 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/auth/authorization_session.h"
+#include "mongo/db/auth/user_name.h"
+#include "mongo/db/logical_session_id_helpers.h"
+#include "mongo/db/pipeline/document_source_list_local_sessions.h"
+#include "mongo/db/pipeline/document_sources_gen.h"
+
+namespace mongo {
+
+REGISTER_DOCUMENT_SOURCE(listLocalSessions,
+ DocumentSourceListLocalSessions::LiteParsed::parse,
+ DocumentSourceListLocalSessions::createFromBson);
+
+const char* DocumentSourceListLocalSessions::kStageName = "$listLocalSessions";
+
+DocumentSource::GetNextResult DocumentSourceListLocalSessions::getNext() {
+ pExpCtx->checkForInterrupt();
+
+ while (!_ids.empty()) {
+ const auto& id = _ids.back();
+ _ids.pop_back();
+
+ const auto& record = _cache->peekCached(id);
+ if (!record) {
+ // It's possible for SessionRecords to have expired while we're walking
+ continue;
+ }
+ return Document(record->toBSON());
+ }
+
+ return GetNextResult::makeEOF();
+}
+
+boost::intrusive_ptr<DocumentSource> DocumentSourceListLocalSessions::createFromBson(
+ BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) {
+
+ uassert(
+ ErrorCodes::InvalidNamespace,
+ str::stream() << kStageName
+ << " must be run against the database with {aggregate: 1}, not a collection",
+ pExpCtx->ns.isCollectionlessAggregateNS());
+
+ return new DocumentSourceListLocalSessions(pExpCtx, listSessionsParseSpec(kStageName, spec));
+}
+
+DocumentSourceListLocalSessions::DocumentSourceListLocalSessions(
+ const boost::intrusive_ptr<ExpressionContext>& pExpCtx, const ListSessionsSpec& spec)
+ : DocumentSource(pExpCtx), _spec(spec) {
+ const auto& opCtx = pExpCtx->opCtx;
+ _cache = LogicalSessionCache::get(opCtx);
+ if (_spec.getAllUsers()) {
+ invariant(!_spec.getUsers() || _spec.getUsers()->empty());
+ _ids = _cache->listIds();
+ } else {
+ _ids = _cache->listIds(listSessionsUsersToDigests(_spec.getUsers().get()));
+ }
+}
+
+namespace {
+ListSessionsUser getUserNameForLoggedInUser(const OperationContext* opCtx) {
+ auto* client = opCtx->getClient();
+
+ ListSessionsUser user;
+ if (AuthorizationManager::get(client->getServiceContext())->isAuthEnabled()) {
+ const auto& userName = AuthorizationSession::get(client)->getSingleUser()->getName();
+ user.setUser(userName.getUser());
+ user.setDb(userName.getDB());
+ } else {
+ user.setUser("");
+ user.setDb("");
+ }
+ return user;
+}
+
+bool operator==(const ListSessionsUser& user1, const ListSessionsUser& user2) {
+ return std::tie(user1.getUser(), user1.getDb()) == std::tie(user2.getUser(), user2.getDb());
+}
+} // namespace
+
+} // namespace mongo
+
+std::vector<mongo::SHA256Block> mongo::listSessionsUsersToDigests(
+ const std::vector<ListSessionsUser>& users) {
+ std::vector<SHA256Block> ret;
+ ret.reserve(users.size());
+ for (const auto& user : users) {
+ ret.push_back(getLogicalSessionUserDigestFor(user.getUser(), user.getDb()));
+ }
+ return ret;
+}
+
+mongo::PrivilegeVector mongo::listSessionsRequiredPrivileges(const ListSessionsSpec& spec) {
+ const auto needsPrivs = ([spec]() {
+ if (spec.getAllUsers()) {
+ return true;
+ }
+ // parseSpec should ensure users is non-empty.
+ invariant(spec.getUsers());
+
+ const auto& myName =
+ getUserNameForLoggedInUser(Client::getCurrent()->getOperationContext());
+ const auto& users = spec.getUsers().get();
+ return !std::all_of(
+ users.cbegin(), users.cend(), [myName](const auto& name) { return myName == name; });
+ })();
+
+ if (needsPrivs) {
+ return {Privilege(ResourcePattern::forClusterResource(), ActionType::listSessions)};
+ } else {
+ return PrivilegeVector();
+ }
+}
+
+mongo::ListSessionsSpec mongo::listSessionsParseSpec(StringData stageName,
+ const BSONElement& spec) {
+ uassert(ErrorCodes::TypeMismatch,
+ str::stream() << stageName << " options must be specified in an object, but found: "
+ << typeName(spec.type()),
+ spec.type() == BSONType::Object);
+
+ IDLParserErrorContext ctx(stageName);
+ auto ret = ListSessionsSpec::parse(ctx, spec.Obj());
+
+ uassert(ErrorCodes::UnsupportedFormat,
+ str::stream() << stageName
+ << " may not specify {allUsers:true} and {users:[...]} at the same time",
+ !ret.getAllUsers() || !ret.getUsers() || ret.getUsers()->empty());
+
+ if (!ret.getAllUsers() && (!ret.getUsers() || ret.getUsers()->empty())) {
+ // Implicit request for self
+ const auto& userName =
+ getUserNameForLoggedInUser(Client::getCurrent()->getOperationContext());
+ ret.setUsers(std::vector<ListSessionsUser>({userName}));
+ }
+
+ return ret;
+}
diff --git a/src/mongo/db/pipeline/document_source_list_local_sessions.h b/src/mongo/db/pipeline/document_source_list_local_sessions.h
new file mode 100644
index 00000000000..8c386f0ecaa
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_list_local_sessions.h
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2017 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "mongo/bson/bsonmisc.h"
+#include "mongo/bson/bsonobj.h"
+#include "mongo/crypto/sha256_block.h"
+#include "mongo/db/logical_session_cache.h"
+#include "mongo/db/pipeline/document_source.h"
+#include "mongo/db/pipeline/document_sources_gen.h"
+#include "mongo/db/pipeline/lite_parsed_document_source.h"
+
+namespace mongo {
+
+ListSessionsSpec listSessionsParseSpec(StringData stageName, const BSONElement& spec);
+PrivilegeVector listSessionsRequiredPrivileges(const ListSessionsSpec& spec);
+std::vector<SHA256Block> listSessionsUsersToDigests(const std::vector<ListSessionsUser>& users);
+
+/**
+ * Produces one document per session in the local cache if 'allUsers' is specified
+ * as true, and returns just sessions for the currently logged in user if
+ * 'allUsers' is specified as false, or not specified at all.
+ */
+class DocumentSourceListLocalSessions final : public DocumentSource {
+public:
+ static const char* kStageName;
+
+ class LiteParsed final : public LiteParsedDocumentSource {
+ public:
+ static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request,
+ const BSONElement& spec) {
+
+ return stdx::make_unique<LiteParsed>(listSessionsParseSpec(kStageName, spec));
+ }
+
+ explicit LiteParsed(const ListSessionsSpec& spec) : _spec(spec) {}
+
+ stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final {
+ return stdx::unordered_set<NamespaceString>();
+ }
+
+ PrivilegeVector requiredPrivileges(bool isMongos) const final {
+ return listSessionsRequiredPrivileges(_spec);
+ }
+
+ bool isInitialSource() const final {
+ return true;
+ }
+
+ bool allowedToForwardFromMongos() const final {
+ return false;
+ }
+
+ private:
+ const ListSessionsSpec _spec;
+ };
+
+ GetNextResult getNext() final;
+
+ const char* getSourceName() const final {
+ return kStageName;
+ }
+
+ Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final {
+ return Value(Document{{getSourceName(), _spec.toBSON()}});
+ }
+
+ StageConstraints constraints() const final {
+ StageConstraints constraints;
+ constraints.requiredPosition = StageConstraints::PositionRequirement::kFirst;
+ constraints.hostRequirement = StageConstraints::HostTypeRequirement::kAnyShardOrMongoS;
+ constraints.requiresInputDocSource = false;
+ constraints.isAllowedInsideFacetStage = false;
+ constraints.isIndependentOfAnyCollection = true;
+ return constraints;
+ }
+
+ static boost::intrusive_ptr<DocumentSource> createFromBson(
+ BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx);
+
+private:
+ DocumentSourceListLocalSessions(const boost::intrusive_ptr<ExpressionContext>& pExpCtx,
+ const ListSessionsSpec& spec);
+
+ const ListSessionsSpec _spec;
+ const LogicalSessionCache* _cache;
+ std::vector<LogicalSessionId> _ids;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/pipeline/document_sources.idl b/src/mongo/db/pipeline/document_sources.idl
index 52f6803f69b..e329a286c5e 100644
--- a/src/mongo/db/pipeline/document_sources.idl
+++ b/src/mongo/db/pipeline/document_sources.idl
@@ -90,6 +90,24 @@ structs:
type: timestamp
description: The timestamp of the logical time
+ ListSessionsUser:
+ description: "A struct representing a $listSessions/$listLocalSessions User"
+ strict: true
+ fields:
+ user: string
+ db: string
+
+ ListSessionsSpec:
+ description: "$listSessions and $listLocalSessions pipeline spec"
+ strict: true
+ fields:
+ allUsers:
+ type: bool
+ default: false
+ users:
+ type: array<ListSessionsUser>
+ optional: true
+
ResumeTokenInternal:
description: The internal format of a resume token. For use by the ResumeToken class
only.
diff --git a/src/mongo/db/pipeline/lite_parsed_document_source.h b/src/mongo/db/pipeline/lite_parsed_document_source.h
index 60e2e8bd8ee..bfe69d6af00 100644
--- a/src/mongo/db/pipeline/lite_parsed_document_source.h
+++ b/src/mongo/db/pipeline/lite_parsed_document_source.h
@@ -111,6 +111,13 @@ public:
virtual bool isInitialSource() const {
return false;
}
+
+ /**
+ * Returns true if this stage may be forwarded to shards from a mongos.
+ */
+ virtual bool allowedToForwardFromMongos() const {
+ return true;
+ }
};
class LiteParsedDocumentSourceDefault final : public LiteParsedDocumentSource {
diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.h b/src/mongo/db/pipeline/lite_parsed_pipeline.h
index a83a1e3c8a0..7a9f7625604 100644
--- a/src/mongo/db/pipeline/lite_parsed_pipeline.h
+++ b/src/mongo/db/pipeline/lite_parsed_pipeline.h
@@ -102,6 +102,15 @@ public:
});
}
+ /**
+ * Returns false if the pipeline has any stage which must be run locally on mongos.
+ */
+ bool allowedToForwardFromMongos() const {
+ return std::all_of(_stageSpecs.cbegin(), _stageSpecs.cend(), [](const auto& spec) {
+ return spec->allowedToForwardFromMongos();
+ });
+ }
+
private:
std::vector<std::unique_ptr<LiteParsedDocumentSource>> _stageSpecs;
};
diff --git a/src/mongo/s/commands/cluster_aggregate.cpp b/src/mongo/s/commands/cluster_aggregate.cpp
index b6c326b53cb..4788de5b5a1 100644
--- a/src/mongo/s/commands/cluster_aggregate.cpp
+++ b/src/mongo/s/commands/cluster_aggregate.cpp
@@ -415,10 +415,10 @@ Status ClusterAggregate::runAggregate(OperationContext* opCtx,
StringMap<ExpressionContext::ResolvedNamespace> resolvedNamespaces;
LiteParsedPipeline liteParsedPipeline(request);
- // TODO SERVER-29141 support $changeStream on mongos.
+ // TODO SERVER-29141 support forcing pipeline to run on Mongos.
uassert(40567,
- "$changeStream is not yet supported on mongos",
- !liteParsedPipeline.hasChangeStream());
+ "Unable to force mongos-only stage to run on mongos",
+ liteParsedPipeline.allowedToForwardFromMongos());
for (auto&& nss : liteParsedPipeline.getInvolvedNamespaces()) {
const auto resolvedNsRoutingInfo =