0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00

SERVER-28335 Implement refreshSessions and refreshSessionsInternal commands

This commit is contained in:
samantharitter 2017-08-14 16:38:25 -04:00
parent aa1c211dc3
commit ea31111dc9
18 changed files with 440 additions and 31 deletions

View File

@ -4465,6 +4465,16 @@ var authCommandsLib = {
command: {refreshLogicalSessionCacheNow: 1},
testcases: [{runOnDb: adminDbName, roles: roles_all}],
},
{
testname: "refreshSessions",
command: {refreshSessions: []},
testcases: [{runOnDb: adminDbName, roles: roles_all}],
},
{
testname: "refreshSessionsInternal",
command: {refreshSessionsInternal: []},
testcases: [{runOnDb: adminDbName, roles: {__system: 1}}],
},
],
/************* SHARED TEST LOGIC ****************/

View File

@ -477,6 +477,8 @@
stageDebug: {skip: isAnInternalCommand},
startSession: {skip: isAnInternalCommand},
refreshLogicalSessionCacheNow: {skip: isAnInternalCommand},
refreshSessions: {skip: isUnrelated},
refreshSessionsInternal: {skip: isAnInternalCommand},
top: {skip: "tested in views/views_stats.js"},
touch: {
command: {touch: "view", data: true},

View File

@ -0,0 +1,84 @@
(function() {
"use strict";
var conn;
var admin;
var result;
var startSession = {startSession: 1};
// Run initial tests without auth.
conn = MongoRunner.runMongod({nojournal: ""});
admin = conn.getDB("admin");
result = admin.runCommand(startSession);
assert.commandWorked(result, "failed to startSession");
var lsid = result.id;
// Test that we can run refreshSessions unauthenticated if --auth is off.
result = admin.runCommand({refreshSessions: [lsid]});
assert.commandWorked(result, "could not run refreshSessions unauthenticated without --auth");
// Test that we can run refreshSessions authenticated if --auth is off.
admin.createUser(
{user: 'admin', pwd: 'admin', roles: ['readAnyDatabase', 'userAdminAnyDatabase']});
admin.auth("admin", "admin");
result = admin.runCommand(startSession);
var lsid2 = result.id;
result = admin.runCommand({refreshSessions: [lsid2]});
assert.commandWorked(result, "could not run refreshSessions logged in with --auth off");
// Turn on auth for further testing.
MongoRunner.stopMongod(conn);
conn = MongoRunner.runMongod({auth: "", nojournal: ""});
admin = conn.getDB("admin");
admin.createUser(
{user: 'admin', pwd: 'admin', roles: ['readAnyDatabase', 'userAdminAnyDatabase']});
admin.auth("admin", "admin");
result = admin.runCommand({
createRole: 'readAdmin',
privileges: [{resource: {db: 'admin', collection: 'system.sessions'}, actions: ['find']}],
roles: []
});
assert.commandWorked(result, "couldn't make readAdmin role");
admin.createUser({user: 'readAdmin', pwd: 'pwd', roles: ['readAdmin']});
admin.logout();
// Test that we cannot run refreshSessions unauthenticated if --auth is on.
result = admin.runCommand({refreshSessions: [lsid]});
assert.commandFailed(result, "able to run refreshSessions without authenticating");
// Test that we can run refreshSessions on our own sessions authenticated if --auth is on.
admin.auth("admin", "admin");
result = admin.runCommand(startSession);
var lsid3 = result.id;
result = admin.runCommand({refreshSessions: [lsid3]});
assert.commandWorked(result, "unable to run refreshSessions while logged in");
// Test that we can refresh "others'" sessions (new ones) when authenticated with --auth.
result = admin.runCommand({refreshSessions: [lsid]});
assert.commandWorked(result, "unable to refresh novel lsids");
// Test that sending a mix of known and new sessions is fine
result = admin.runCommand({refreshSessions: [lsid, lsid2, lsid3]});
assert.commandWorked(result, "unable to refresh mix of known and unknown lsids");
// Test that sending a set of sessions with duplicates is fine
result = admin.runCommand({refreshSessions: [lsid, lsid, lsid, lsid]});
assert.commandWorked(result, "unable to refresh with duplicate lsids in the set");
// Test that we can run refreshSessions with an empty set of sessions.
result = admin.runCommand({refreshSessions: []});
assert.commandWorked(result, "unable to refresh empty set of lsids");
// Test that once we force a refresh, all of these sessions are in the sessions collection.
admin.logout();
admin.auth("readAdmin", "pwd");
result = admin.runCommand({refreshLogicalSessionCacheNow: 1});
assert.commandWorked(result, "could not force refresh");
assert.eq(admin.system.sessions.count(), 3, "should have refreshed all session records");
MongoRunner.stopMongod(conn);
})();

View File

@ -0,0 +1,38 @@
(function() {
"use strict";
var conn;
var admin;
conn = MongoRunner.runMongod({auth: "", nojournal: ""});
admin = conn.getDB("admin");
admin.createUser({user: 'admin', pwd: 'admin', roles: jsTest.adminUserRoles});
admin.auth("admin", "admin");
result = admin.runCommand({
createRole: 'impersonate',
privileges: [{resource: {cluster: true}, actions: ['impersonate']}],
roles: []
});
assert.commandWorked(result, "couldn't make impersonate role");
admin.createUser({user: 'internal', pwd: 'pwd', roles: ['impersonate']});
// Test that we cannot run refreshSessions unauthenticated if --auth is on.
var result = admin.runCommand({refreshSessionsInternal: []});
assert.commandFailed(result, "able to run refreshSessionsInternal without authenticating");
// Test that we cannot run refreshSessionsInternal without impersonate privileges.
admin.auth("admin", "admin");
result = admin.runCommand({refreshSessionsInternal: []});
assert.commandFailed(result, "able to run refreshSessions without impersonate privileges");
admin.logout();
// Test that we can run refreshSessionsInternal if we can impersonate.
admin.auth("internal", "pwd");
result = admin.runCommand({refreshSessionsInternal: []});
assert.commandWorked(result, "unable to run command with impersonate privileges");
MongoRunner.stopMongod(conn);
})();

View File

@ -253,6 +253,8 @@
profile: {skip: "primary only"},
reIndex: {skip: "does not return user data"},
refreshLogicalSessionCacheNow: {skip: "does not return user data"},
refreshSessions: {skip: "does not return user data"},
refreshSessionsInternal: {skip: "does not return user data"},
removeShard: {skip: "primary only"},
removeShardFromZone: {skip: "primary only"},
renameCollection: {skip: "primary only"},

View File

@ -258,6 +258,8 @@
profile: {skip: "primary only"},
reIndex: {skip: "does not return user data"},
refreshLogicalSessionCacheNow: {skip: "does not return user data"},
refreshSessions: {skip: "does not return user data"},
refreshSessionsInternal: {skip: "does not return user data"},
removeShard: {skip: "primary only"},
removeShardFromZone: {skip: "primary only"},
renameCollection: {skip: "primary only"},

View File

@ -257,6 +257,8 @@
planCacheSetFilter: {skip: "does not return user data"},
profile: {skip: "primary only"},
refreshLogicalSessionCacheNow: {skip: "does not return user data"},
refreshSessions: {skip: "does not return user data"},
refreshSessionsInternal: {skip: "does not return user data"},
reIndex: {skip: "does not return user data"},
removeShard: {skip: "primary only"},
removeShardFromZone: {skip: "primary only"},

View File

@ -872,6 +872,7 @@ env.Library(
source=[
'logical_session_id.cpp',
env.Idlc('logical_session_id.idl')[0],
env.Idlc('refresh_sessions.idl')[0],
],
LIBDEPS=[
'$BUILD_DIR/mongo/base',

View File

@ -66,6 +66,8 @@ env.Library(
"mr_common.cpp",
"parameters.cpp",
"refresh_logical_session_cache_now.cpp",
"refresh_sessions_command.cpp",
"refresh_sessions_command_internal.cpp",
"rename_collection_common.cpp",
"start_session_command.cpp",
"user_management_commands_common.cpp",

View File

@ -0,0 +1,91 @@
/**
* 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/base/init.h"
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/client.h"
#include "mongo/db/commands.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/logical_session_cache.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/refresh_sessions_gen.h"
namespace mongo {
class RefreshSessionsCommand final : public BasicCommand {
MONGO_DISALLOW_COPYING(RefreshSessionsCommand);
public:
RefreshSessionsCommand() : BasicCommand("refreshSessions") {}
bool slaveOk() const override {
return true;
}
bool adminOnly() const override {
return false;
}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return false;
}
void help(std::stringstream& help) const override {
help << "renew a set of logical sessions";
}
Status checkAuthForOperation(OperationContext* opCtx,
const std::string& dbname,
const BSONObj& cmdObj) override {
// It is always ok to run this command, as long as you are authenticated
// as some user, if auth is enabled.
AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient());
try {
auto user = authSession->getSingleUser();
invariant(user);
return Status::OK();
} catch (...) {
return exceptionToStatus();
}
}
virtual bool run(OperationContext* opCtx,
const std::string& db,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
IDLParserErrorContext ctx("RefreshSessionsCmdFromClient");
auto cmd = RefreshSessionsCmdFromClient::parse(ctx, cmdObj);
auto res =
LogicalSessionCache::get(opCtx->getServiceContext())->refreshSessions(opCtx, cmd);
if (!res.isOK()) {
return appendCommandStatus(result, res);
}
return true;
}
} refreshSessionsCommand;
} // namespace mongo

View File

@ -0,0 +1,88 @@
/**
* 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/base/init.h"
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/client.h"
#include "mongo/db/commands.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/logical_session_cache.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/refresh_sessions_gen.h"
namespace mongo {
class RefreshSessionsCommandInternal final : public BasicCommand {
MONGO_DISALLOW_COPYING(RefreshSessionsCommandInternal);
public:
RefreshSessionsCommandInternal() : BasicCommand("refreshSessionsInternal") {}
bool slaveOk() const override {
return true;
}
bool adminOnly() const override {
return false;
}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return false;
}
void help(std::stringstream& help) const override {
help << "renew a set of logical sessions";
}
Status checkAuthForOperation(OperationContext* opCtx,
const std::string& dbname,
const BSONObj& cmdObj) override {
// Must be authenticated as an internal cluster member.
auto authSession = AuthorizationSession::get(opCtx->getClient());
if (!authSession->isAuthorizedForPrivilege(
Privilege(ResourcePattern::forClusterResource(), ActionType::impersonate))) {
return {ErrorCodes::Unauthorized, "unauthorized"};
}
return Status::OK();
}
virtual bool run(OperationContext* opCtx,
const std::string& db,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
IDLParserErrorContext ctx("RefreshSessionsCmdFromClusterMember");
auto cmd = RefreshSessionsCmdFromClusterMember::parse(ctx, cmdObj);
auto res =
LogicalSessionCache::get(opCtx->getServiceContext())->refreshSessions(opCtx, cmd);
if (!res.isOK()) {
return appendCommandStatus(result, res);
}
return true;
}
} refreshSessionsCommandInternal;
} // namespace mongo

View File

@ -130,35 +130,51 @@ Status LogicalSessionCache::promote(LogicalSessionId lsid) {
return {ErrorCodes::NoSuchSession, "no matching session record found in the cache"};
}
// Do not use records if they have expired.
auto time = now();
if (_isDead(it->second, time)) {
return {ErrorCodes::NoSuchSession, "no matching session record found in the cache"};
}
// Update the last use time before returning.
it->second.setLastUse(time);
it->second.setLastUse(now());
return Status::OK();
}
Status LogicalSessionCache::startSession(OperationContext* opCtx, LogicalSessionRecord record) {
// Add the new record to our local cache. We will insert it into the sessions collection
// the next time _refresh is called.
// the next time _refresh is called. If there is already a record in the cache for this
// session, we'll just write over it with our newer, more recent one.
_addToCache(record);
return Status::OK();
}
// If we get a conflict here, then an interloper may have ended this session
// and then created a new one with the same id. In this case, return a failure.
auto oldRecord = _addToCache(record);
if (oldRecord) {
if (*oldRecord != record) {
if (!_isDead(*oldRecord, now())) {
return {ErrorCodes::DuplicateSession, "session with this id already exists"};
}
Status LogicalSessionCache::refreshSessions(OperationContext* opCtx,
const RefreshSessionsCmdFromClient& cmd) {
// Update the timestamps of all these records in our cache.
auto sessions = makeLogicalSessionIds(cmd.getRefreshSessions(), opCtx);
for (auto& lsid : sessions) {
if (!promote(lsid).isOK()) {
// This is a new record, insert it.
_addToCache(makeLogicalSessionRecord(opCtx, lsid, now()));
}
}
return Status::OK();
}
Status LogicalSessionCache::refreshSessions(OperationContext* opCtx,
const RefreshSessionsCmdFromClusterMember& cmd) {
LogicalSessionRecordSet toRefresh{};
// Update the timestamps of all these records in our cache.
auto records = cmd.getRefreshSessionsInternal();
for (auto& record : records) {
if (!promote(record.getId()).isOK()) {
// This is a new record, insert it.
_addToCache(record);
}
toRefresh.insert(record);
}
// Write to the sessions collection now.
return _sessionsColl->refreshSessions(opCtx, toRefresh, now());
}
void LogicalSessionCache::refreshNow(Client* client) {
return _refresh(client);
}

View File

@ -30,6 +30,7 @@
#include "mongo/base/status_with.h"
#include "mongo/db/logical_session_id.h"
#include "mongo/db/refresh_sessions_gen.h"
#include "mongo/db/service_liason.h"
#include "mongo/db/sessions_collection.h"
#include "mongo/db/time_proof_service.h"
@ -113,9 +114,7 @@ public:
/**
* If the cache contains a record for this LogicalSessionId, promotes that lsid
* to be the most recently used and updates its lastUse date to be the current
* time. Otherwise, returns an error.
*
* This method does not issue networking calls.
* time. Returns an error if the session was not found.
*/
Status promote(LogicalSessionId lsid);
@ -136,6 +135,13 @@ public:
*/
Status startSession(OperationContext* opCtx, LogicalSessionRecord record);
/**
* Refresh the given sessions. Updates the timestamps of these records in
* the local cache.
*/
Status refreshSessions(OperationContext* opCtx, const RefreshSessionsCmdFromClient& cmd);
Status refreshSessions(OperationContext* opCtx, const RefreshSessionsCmdFromClusterMember& cmd);
/**
* Removes all local records in this cache. Does not remove the corresponding
* authoritative session records from the sessions collection.

View File

@ -205,10 +205,10 @@ TEST_F(LogicalSessionCacheTest, FetchUpdatesLastUse) {
res = cache()->promote(lsid);
ASSERT(res.isOK());
// Let record expire, we should not be able to get it from the cache
// Let record expire, we should still be able to get it, since cache didn't get cleared
service()->fastForward(kSessionTimeout + Milliseconds(1));
res = cache()->promote(lsid);
ASSERT(!res.isOK());
ASSERT(res.isOK());
}
// Test the startSession method
@ -311,8 +311,10 @@ TEST_F(LogicalSessionCacheTest, BasicSessionExpiration) {
service()->fastForward(Milliseconds(kSessionTimeout.count() + 5));
// Check that it is no longer in the cache
cache()->refreshNow(client());
res = cache()->promote(record.getId());
ASSERT(!res.isOK());
// TODO SERVER-29709
// ASSERT(!res.isOK());
}
// Test that we keep refreshing sessions that are active on the service

View File

@ -28,7 +28,7 @@
#include "mongo/platform/basic.h"
#include "mongo/db/logical_session_id.h"
#include "mongo/db/logical_session_id_helpers.h"
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/auth/user.h"
@ -64,7 +64,8 @@ SHA256Block lookupUserDigest(OperationContext* opCtx) {
} // namespace
LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& fromClient,
OperationContext* opCtx) {
OperationContext* opCtx,
std::initializer_list<Privilege> allowSpoof) {
LogicalSessionId lsid;
lsid.setId(fromClient.getId());
@ -74,8 +75,14 @@ LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& fromClient
uassert(ErrorCodes::Unauthorized,
"Unauthorized to set user digest in LogicalSessionId",
authSession->isAuthorizedForPrivilege(
Privilege(ResourcePattern::forClusterResource(), ActionType::impersonate)));
std::any_of(allowSpoof.begin(),
allowSpoof.end(),
[&](const auto& priv) {
return authSession->isAuthorizedForPrivilege(priv);
}) ||
authSession->isAuthorizedForPrivilege(Privilege(
ResourcePattern::forClusterResource(), ActionType::impersonate)) ||
lookupUserDigest(opCtx) == fromClient.getUid());
lsid.setUid(*fromClient.getUid());
} else {
@ -185,4 +192,16 @@ void initializeOperationSessionInfo(OperationContext* opCtx,
}
}
LogicalSessionIdSet makeLogicalSessionIds(const std::vector<LogicalSessionFromClient>& sessions,
OperationContext* opCtx,
std::initializer_list<Privilege> allowSpoof) {
LogicalSessionIdSet lsids;
lsids.reserve(sessions.size());
for (auto&& session : sessions) {
lsids.emplace(makeLogicalSessionId(session, opCtx, allowSpoof));
}
return lsids;
}
} // namespace mongo

View File

@ -28,6 +28,10 @@
#pragma once
#include <initializer_list>
#include <vector>
#include "mongo/db/auth/privilege.h"
#include "mongo/db/logical_session_id.h"
namespace mongo {
@ -36,7 +40,8 @@ namespace mongo {
* Factory functions to generate logical session records.
*/
LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& lsid,
OperationContext* opCtx);
OperationContext* opCtx,
std::initializer_list<Privilege> allowSpoof = {});
LogicalSessionId makeLogicalSessionId(OperationContext* opCtx);
/**
@ -51,6 +56,9 @@ LogicalSessionRecord makeLogicalSessionRecord(OperationContext* opCtx,
Date_t lastUse);
LogicalSessionToClient makeLogicalSessionToClient(const LogicalSessionId& lsid);
LogicalSessionIdSet makeLogicalSessionIds(const std::vector<LogicalSessionFromClient>& sessions,
OperationContext* opCtx,
std::initializer_list<Privilege> allowSpoof = {});
/**
* Parses the session information from the body of a request and installs it on the current

View File

@ -0,0 +1,39 @@
# 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/>.
#
# This IDL file describes the BSON format for a LogicalSessionId, and
# handles the serialization to and deserialization from its BSON representation
# for that class.
global:
cpp_namespace: "mongo"
imports:
- "mongo/idl/basic_types.idl"
- "mongo/db/logical_session_id.idl"
structs:
RefreshSessionsCmdFromClient:
description: "A struct representing a refreshSessions command from a client"
strict: false
fields:
refreshSessions: array<LogicalSessionFromClient>
RefreshSessionsCmdFromClusterMember:
description: "A struct representing a refreshSessions command from a cluster member"
strict: false
fields:
refreshSessionsInternal: array<LogicalSessionRecord>

View File

@ -60,10 +60,7 @@ public:
/**
* Updates the last-use times on the given sessions to be greater than
* or equal to the given time.
*
* Returns a list of sessions for which no authoritative record was found,
* and hence were not refreshed. Returns an error if a networking issue occurred.
* or equal to the given time. Returns an error if a networking issue occurred.
*/
virtual Status refreshSessions(OperationContext* opCtx,
const LogicalSessionRecordSet& sessions,