Skip to content

Commit

Permalink
SERVER-29628 $listSessions aggregation stage
Browse files Browse the repository at this point in the history
  • Loading branch information
sgolemon-corp committed Aug 23, 2017
1 parent a09f198 commit 0b09cf4
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ selector:
- 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
- jstests/core/list_all_sessions.js # Too many users authenticated
- jstests/core/list_sessions.js # Too many users authenticated

executor:
config:
Expand Down
22 changes: 22 additions & 0 deletions jstests/auth/lib/commands_lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,28 @@ var authCommandsLib = {
testcases: [{runOnDb: adminDbName, roles: roles_all}],
skipSharded: true
},
{
testname: "aggregate_listSessions_allUsers_true",
command: {
aggregate: 'system.sessions',
pipeline: [{$listSessions: {allUsers: true}}],
cursor: {}
},
testcases: [{
runOnDb: adminDbName,
roles:
{clusterAdmin: 1, clusterMonitor: 1, clusterManager: 1, root: 1, __system: 1}
}]
},
{
testname: "aggregate_listSessions_allUsers_false",
command: {
aggregate: 'system.sessions',
pipeline: [{$listSessions: {allUsers: false}}],
cursor: {}
},
testcases: [{runOnDb: adminDbName, roles: roles_all}]
},
{
testname: "aggregate_lookup",
command: {
Expand Down
57 changes: 57 additions & 0 deletions jstests/auth/list_all_sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Auth tests for the $listSessions {allUsers:true} aggregation stage.

(function() {
'use strict';
load('jstests/aggregation/extras/utils.js');

function runListAllSessionsTest(mongod) {
assert(mongod);
const admin = mongod.getDB("admin");

const pipeline = [{'$listSessions': {allUsers: true}}];
function listSessions() {
return admin.system.sessions.aggregate(pipeline);
}

admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
assert(admin.auth('admin', 'pass'));
admin.createUser({user: 'user1', pwd: 'pass', roles: jsTest.basicUserRoles});
admin.logout();

// Fail if we're not logged in.
assertErrorCode(admin.system.sessions, pipeline, ErrorCodes.Unauthorized);

// Start a new session and capture its sessionId.
assert(admin.auth('user1', 'pass'));
const myid = assert.commandWorked(admin.runCommand({startSession: 1})).id.id;
assert(myid !== undefined);
assert.commandWorked(admin.runCommand({refreshLogicalSessionCacheNow: 1}));

// Ensure that a normal user can NOT listSessions{allUsers:true} to view their session.
assertErrorCode(admin.system.sessions, pipeline, ErrorCodes.Unauthorized);

// Ensure that a normal user can NOT listSessions to view others' sessions.
const viewAdminPipeline = [{'$listSessions': {users: [{user: 'admin', db: 'admin'}]}}];
assertErrorCode(admin.system.sessions, viewAdminPipeline, ErrorCodes.Unauthorized);

// Ensure that the cache now contains the session and is visible by admin
assert(admin.auth('admin', 'pass'));
const resultArray = listSessions().toArray();
assert.eq(resultArray.length, 1);
const cacheid = resultArray[0]._id.id;
assert(cacheid !== undefined);
assert.eq(0, bsonWoCompare({x: cacheid}, {x: myid}));

// Make sure pipelining other collections fail.
assertErrorCode(admin.system.collections, pipeline, ErrorCodes.InvalidNamespace);
}

const mongod = MongoRunner.runMongod({auth: ""});
runListAllSessionsTest(mongod);
MongoRunner.stopMongod(mongod);

const st =
new ShardingTest({shards: 1, mongos: 1, config: 1, other: {keyFile: 'jstests/libs/key1'}});
runListAllSessionsTest(st.s0);
st.stop();
})();
75 changes: 75 additions & 0 deletions jstests/auth/list_sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Auth tests for the $listSessions aggregation pipeline.

(function() {
'use strict';
load('jstests/aggregation/extras/utils.js');

function runListSessionsTest(mongod) {
assert(mongod);
const admin = mongod.getDB('admin');

const pipeline = [{'$listSessions': {}}];
function listSessions() {
return admin.system.sessions.aggregate(pipeline);
}

admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
assert(admin.auth('admin', 'pass'));

admin.createUser({user: 'user1', pwd: 'pass', roles: jsTest.basicUserRoles});
admin.createUser({user: 'user2', pwd: 'pass', roles: jsTest.basicUserRoles});
admin.logout();

// Fail when not logged in.
assertErrorCode(admin.system.sessions, pipeline, ErrorCodes.Unauthorized);

// Start a new session and capture its sessionId.
assert(admin.auth('user1', 'pass'));
const myid = assert.commandWorked(admin.runCommand({startSession: 1})).id.id;
assert(myid !== undefined);

// Sync cache to collection and ensure it arrived.
assert.commandWorked(admin.runCommand({refreshLogicalSessionCacheNow: 1}));
const resultArray = listSessions().toArray();
assert.eq(resultArray.length, 1);
const cacheid = resultArray[0]._id.id;
assert(cacheid !== undefined);
assert.eq(bsonWoCompare(cacheid, myid), 0);

// Ask again using explicit UID.
const user1Pipeline = [{'$listSessions': {users: [{user: "user1", db: "admin"}]}}];
function listUser1Sessions() {
return admin.system.sessions.aggregate(user1Pipeline);
}
const resultArrayMine = listUser1Sessions().toArray();
assert.eq(bsonWoCompare(resultArray, resultArrayMine), 0);

// Make sure pipelining other collections fail
assertErrorCode(admin.system.collections, pipeline, ErrorCodes.InvalidNamespace);

// Ensure that changing users hides the session everwhere.
assert(admin.auth('user2', 'pass'));
assert.eq(listSessions().toArray().length, 0);

// Ensure users can't view either other's sessions.
assertErrorCode(admin.system.sessions, user1Pipeline, ErrorCodes.Unauthorized);

if (true) {
// TODO SERVER-29141: Support forcing pipelines to run on mongos
return;
}
function listLocalSessions() {
return admin.aggregate([{'$listLocalSessions': {}}]);
}
assert.eq(listLocalSessions().toArray().length, 0);
}

const mongod = MongoRunner.runMongod({auth: ""});
runListSessionsTest(mongod);
MongoRunner.stopMongod(mongod);

const st =
new ShardingTest({shards: 1, mongos: 1, config: 1, other: {keyFile: 'jstests/libs/key1'}});
runListSessionsTest(st.s0);
st.stop();
})();
36 changes: 36 additions & 0 deletions jstests/core/list_all_sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Basic tests for the $listSessions {allUsers:true} aggregation stage.

(function() {
'use strict';
load('jstests/aggregation/extras/utils.js');

const admin = db.getSiblingDB("admin");
const pipeline = [{'$listSessions': {allUsers: true}}];
function listSessions() {
return admin.system.sessions.aggregate(pipeline);
}

// Start a new session and capture its sessionId.
const myid = assert.commandWorked(admin.runCommand({startSession: 1})).id.id;
assert(myid !== undefined);
assert.commandWorked(admin.runCommand({refreshLogicalSessionCacheNow: 1}));

// Ensure that the cache now contains the session and is visible by admin.
assert.soon(function() {
const resultArray = listSessions().toArray();
if (resultArray.length < 1) {
return false;
}
const resultArrayMine = resultArray
.map(function(sess) {
return sess._id.id;
})
.filter(function(id) {
return 0 == bsonWoCompare({x: id}, {x: myid});
});
return resultArrayMine.length == 1;
}, "Failed to locate session in collection");

// Make sure pipelining other collections fail.
assertErrorCode(admin.system.collections, pipeline, ErrorCodes.InvalidNamespace);
})();
57 changes: 57 additions & 0 deletions jstests/core/list_sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Basic tests for the $listSessions aggregation stage.

(function() {
'use strict';
load('jstests/aggregation/extras/utils.js');

const admin = db.getSiblingDB('admin');
const pipeline = [{'$listSessions': {}}];
function listSessions() {
return admin.system.sessions.aggregate(pipeline);
}

// Start a new session and capture its sessionId.
const myid = assert.commandWorked(admin.runCommand({startSession: 1})).id.id;
assert(myid !== undefined);

// Sync cache to collection and ensure it arrived.
assert.commandWorked(admin.runCommand({refreshLogicalSessionCacheNow: 1}));
var resultArray;
assert.soon(function() {
resultArray = listSessions().toArray();
if (resultArray.length < 1) {
return false;
}
const resultArrayMine = resultArray
.map(function(sess) {
return sess._id.id;
})
.filter(function(id) {
return 0 == bsonWoCompare({x: id}, {x: myid});
});
return resultArrayMine.length == 1;
}, "Failed to locate session in collection");

// 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 listMySessions() {
return admin.system.sessions.aggregate([{'$listSessions': {users: [myusername]}}]);
}
const myArray = listMySessions().toArray();
assert.eq(resultArray.length, myArray.length);

// Make sure pipelining other collections fail.
assertErrorCode(admin.system.collections, pipeline, ErrorCodes.InvalidNamespace);
})();
1 change: 1 addition & 0 deletions src/mongo/db/pipeline/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ docSourceEnv.Library(
'document_source_internal_split_pipeline.cpp',
'document_source_limit.cpp',
'document_source_list_local_sessions.cpp',
'document_source_list_sessions.cpp',
'document_source_match.cpp',
'document_source_merge_cursors.cpp',
'document_source_mock.cpp',
Expand Down
75 changes: 75 additions & 0 deletions src/mongo/db/pipeline/document_source_list_sessions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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/bson/bsonobj.h"
#include "mongo/db/logical_session_id_helpers.h"
#include "mongo/db/matcher/expression_parser.h"
#include "mongo/db/matcher/extensions_callback_noop.h"
#include "mongo/db/pipeline/document_source_list_sessions.h"
#include "mongo/db/pipeline/document_sources_gen.h"
#include "mongo/db/sessions_collection.h"

namespace mongo {

const char* DocumentSourceListSessions::kStageName = "$listSessions";

REGISTER_DOCUMENT_SOURCE(listSessions,
DocumentSourceListSessions::LiteParsed::parse,
DocumentSourceListSessions::createFromBson);

boost::intrusive_ptr<DocumentSource> DocumentSourceListSessions::createFromBson(
BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) {
const NamespaceString& nss = pExpCtx->ns;

uassert(ErrorCodes::InvalidNamespace,
str::stream() << kStageName << " must be run against the '"
<< SessionsCollection::kSessionsCollection
<< "' collection in the '"
<< SessionsCollection::kSessionsDb
<< "' database.",
nss == NamespaceString{SessionsCollection::kSessionsFullNS});

const auto& spec = listSessionsParseSpec(kStageName, elem);
if (spec.getAllUsers()) {
// No filtration. optimize() should later skip us.
return new DocumentSourceListSessions(BSONObj(), pExpCtx, spec);
}
invariant(spec.getUsers() && !spec.getUsers()->empty());

BSONArrayBuilder builder;
for (const auto& uid : listSessionsUsersToDigests(spec.getUsers().get())) {
ConstDataRange cdr = uid.toCDR();
builder.append(BSONBinData(cdr.data(), cdr.length(), BinDataGeneral));
}
const auto& query = BSON("_id.uid" << BSON("$in" << builder.arr()));
return new DocumentSourceListSessions(query, pExpCtx, spec);
}

} // namespace mongo
Loading

0 comments on commit 0b09cf4

Please sign in to comment.