summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVarun Ravichandran <varun.ravichandran@mongodb.com>2022-11-11 19:49:15 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2022-12-16 23:29:04 +0000
commit946cecc98dfb0047855add41fc344a1ffbb2baa9 (patch)
tree29ccefaeb61f3066f1344bb6827bb384ab0326c2
parentab506f144f44e24addaccbcb755b8d99e7ef29c3 (diff)
downloadmongo-946cecc98dfb0047855add41fc344a1ffbb2baa9.tar.gz
SERVER-70701: Allow AuthorizationSession to enforce expiration times
-rw-r--r--jstests/auth/impersonation-deny.js2
-rw-r--r--jstests/replsets/auth1.js2
-rw-r--r--src/mongo/base/error_codes.yml1
-rw-r--r--src/mongo/db/auth/access_checks.idl2
-rw-r--r--src/mongo/db/auth/authorization_session.h11
-rw-r--r--src/mongo/db/auth/authorization_session_impl.cpp85
-rw-r--r--src/mongo/db/auth/authorization_session_impl.h16
-rw-r--r--src/mongo/db/auth/authorization_session_test.cpp381
-rw-r--r--src/mongo/db/auth/sasl_commands.cpp5
-rw-r--r--src/mongo/db/auth/sasl_mechanism_registry.h9
-rw-r--r--src/mongo/db/auth/security_token_authentication_guard.cpp3
-rw-r--r--src/mongo/db/commands.cpp12
-rw-r--r--src/mongo/db/commands/authentication_commands.cpp2
-rw-r--r--src/mongo/db/commands_test.cpp188
-rw-r--r--src/mongo/db/session/logical_session_id_test.cpp4
-rw-r--r--src/mongo/embedded/embedded_auth_session.cpp8
16 files changed, 584 insertions, 147 deletions
diff --git a/jstests/auth/impersonation-deny.js b/jstests/auth/impersonation-deny.js
index 716094efa8e..3f1f16eabfe 100644
--- a/jstests/auth/impersonation-deny.js
+++ b/jstests/auth/impersonation-deny.js
@@ -18,7 +18,7 @@ function testMongod(mongod, systemuserpwd = undefined) {
// Localhost authbypass is disabled, and we haven't logged in,
// so normal auth-required commands should fail.
- assertUnauthorized({usersInfo: 1}, 'command usersInfo requires authentication');
+ assertUnauthorized({usersInfo: 1}, 'Command usersInfo requires authentication');
// Hello command requires no auth, so it works fine.
assert.commandWorked(admin.runCommand({hello: 1}));
diff --git a/jstests/replsets/auth1.js b/jstests/replsets/auth1.js
index 2b1225dc2bb..8e45f6b403e 100644
--- a/jstests/replsets/auth1.js
+++ b/jstests/replsets/auth1.js
@@ -93,7 +93,7 @@ function doQueryOn(p) {
r = p.getDB("test").foo.findOne();
}, [], "find did not throw, returned: " + tojson(r)).toString();
printjson(error);
- assert.gt(error.indexOf("command find requires authentication"), -1, "error was non-auth");
+ assert.gt(error.indexOf("Command find requires authentication"), -1, "error was non-auth");
}
doQueryOn(secondary);
diff --git a/src/mongo/base/error_codes.yml b/src/mongo/base/error_codes.yml
index a7b20977ce3..e1054f1b471 100644
--- a/src/mongo/base/error_codes.yml
+++ b/src/mongo/base/error_codes.yml
@@ -512,6 +512,7 @@ error_codes:
- {code: 390, name: InvalidSignature}
+ - {code: 391, name: ReauthenticationRequired}
# Error codes 4000-8999 are reserved.
# Non-sequential error codes for compatibility only)
diff --git a/src/mongo/db/auth/access_checks.idl b/src/mongo/db/auth/access_checks.idl
index 3fd6549a842..985c896f76f 100644
--- a/src/mongo/db/auth/access_checks.idl
+++ b/src/mongo/db/auth/access_checks.idl
@@ -52,6 +52,6 @@ enums:
kIsCoauthorizedWith : "is_coauthorized_with"
kIsCoauthorizedWithClient : "is_coauthorized_with_client"
kIsImpersonating : "is_impersonating"
+ kIsUsingLocalhostBypass : "is_using_localhost_bypass" # Called in common code in commands.cpp dispatch
kLookupUser : "lookup_user"
kShouldIgnoreAuthChecks : "should_ignore_auth_checks"
- kIsUsingLocalhostBypass : "is_using_localhost_bypass" # Called in common code in commands.cpp dispatch
diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h
index 777da1ad56d..72d107e5253 100644
--- a/src/mongo/db/auth/authorization_session.h
+++ b/src/mongo/db/auth/authorization_session.h
@@ -144,7 +144,9 @@ public:
* Adds the User identified by "UserName" to the authorization session, acquiring privileges
* for it in the process.
*/
- virtual Status addAndAuthorizeUser(OperationContext* opCtx, const UserName& userName) = 0;
+ virtual Status addAndAuthorizeUser(OperationContext* opCtx,
+ const UserName& userName,
+ boost::optional<Date_t> expirationTime) = 0;
// Returns the authenticated user with the given name. Returns NULL
// if no such user is found.
@@ -182,7 +184,8 @@ public:
// How the active session is authenticated.
enum class AuthenticationMode {
kNone, // Not authenticated.
- kConnection, // For the duration of the connection, or until logged out.
+ kConnection, // For the duration of the connection, or until logged out or
+ // expiration.
kSecurityToken, // By operation scoped security token.
};
virtual AuthenticationMode getAuthenticationMode() const = 0;
@@ -306,6 +309,10 @@ public:
// resource.
virtual bool mayBypassWriteBlockingMode() const = 0;
+ // Returns true if the authorization session is expired. When this returns true,
+ // isAuthenticated() is also expected to return false.
+ virtual bool isExpired() const = 0;
+
protected:
virtual std::tuple<boost::optional<UserName>*, std::vector<RoleName>*> _getImpersonations() = 0;
};
diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp
index 90083b297ba..981e3649bdd 100644
--- a/src/mongo/db/auth/authorization_session_impl.cpp
+++ b/src/mongo/db/auth/authorization_session_impl.cpp
@@ -175,7 +175,6 @@ AuthorizationManager& AuthorizationSessionImpl::getAuthorizationManager() {
void AuthorizationSessionImpl::startRequest(OperationContext* opCtx) {
_externalState->startRequest(opCtx);
- _refreshUserInfoAsNeeded(opCtx);
if (_authenticationMode == AuthenticationMode::kSecurityToken) {
// Previously authenticated using SecurityToken,
// clear that user and reset to unauthenticated state.
@@ -188,7 +187,18 @@ void AuthorizationSessionImpl::startRequest(OperationContext* opCtx) {
_updateInternalAuthorizationState();
}
_authenticationMode = AuthenticationMode::kNone;
+ } else {
+ // For non-security token users, check if expiration has passed and move session into
+ // expired state if so.
+ if (_expirationTime &&
+ _expirationTime.value() <= opCtx->getServiceContext()->getFastClockSource()->now()) {
+ _expiredUserName = std::exchange(_authenticatedUser, boost::none).value()->getName();
+ _expirationTime = boost::none;
+ clearImpersonatedUserData();
+ _updateInternalAuthorizationState();
+ }
}
+ _refreshUserInfoAsNeeded(opCtx);
}
void AuthorizationSessionImpl::startContractTracking() {
@@ -200,7 +210,8 @@ void AuthorizationSessionImpl::startContractTracking() {
}
Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx,
- const UserName& userName) try {
+ const UserName& userName,
+ boost::optional<Date_t> expirationTime) try {
// Check before we start to reveal as little as possible. Note that we do not need the lock
// because only the Client thread can mutate _authenticatedUser.
if (_authenticatedUser) {
@@ -229,6 +240,16 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx,
<< "Already authenticated as: " << previousUser);
}
MONGO_UNREACHABLE;
+ } else {
+ // If session is expired, then treat this as reauth for an expired session and only permit
+ // the same user.
+ if (_expiredUserName) {
+ uassert(7070100,
+ str::stream() << "Only same user is permitted to re-auth to an expired "
+ "session. Expired user is "
+ << _expiredUserName.value(),
+ _expiredUserName == userName);
+ }
}
AuthorizationManager* authzManager = AuthorizationManager::get(opCtx->getServiceContext());
@@ -254,10 +275,20 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx,
uassert(6161502,
"Attempt to authorize a user other than that present in the security token",
validatedTenancyScope->authenticatedUser() == userName);
+ uassert(7070101,
+ "Attempt to set expiration policy on a security token user",
+ expirationTime == boost::none);
validateSecurityTokenUserPrivileges(user->getPrivileges());
_authenticationMode = AuthenticationMode::kSecurityToken;
} else {
+ uassert(7070102,
+ "Invalid expiration time specified",
+ !expirationTime ||
+ expirationTime.value() >
+ opCtx->getServiceContext()->getFastClockSource()->now());
_authenticationMode = AuthenticationMode::kConnection;
+ _expirationTime = std::move(expirationTime);
+ _expiredUserName = boost::none;
}
_authenticatedUser = std::move(user);
@@ -313,13 +344,20 @@ void AuthorizationSessionImpl::logoutAllDatabases(Client* client, StringData rea
"May not log out while using a security token based authentication",
_authenticationMode != AuthenticationMode::kSecurityToken);
- auto user = std::exchange(_authenticatedUser, boost::none);
- if (user == boost::none) {
+ auto authenticatedUser = std::exchange(_authenticatedUser, boost::none);
+ auto expiredUserName = std::exchange(_expiredUserName, boost::none);
+
+ if (authenticatedUser) {
+ auto names = BSON_ARRAY(authenticatedUser.value()->getName().toBSON());
+ audit::logLogout(client, reason, names, BSONArray());
+ } else if (expiredUserName) {
+ auto names = BSON_ARRAY(expiredUserName.value().toBSON());
+ audit::logLogout(client, reason, names, BSONArray());
+ } else {
return;
}
- auto names = BSON_ARRAY(user.value()->getName().toBSON());
- audit::logLogout(client, reason, names, BSONArray());
+ _expirationTime = boost::none;
clearImpersonatedUserData();
_updateInternalAuthorizationState();
@@ -329,22 +367,15 @@ void AuthorizationSessionImpl::logoutAllDatabases(Client* client, StringData rea
void AuthorizationSessionImpl::logoutDatabase(Client* client,
StringData dbname,
StringData reason) {
- stdx::lock_guard<Client> lk(*client);
-
- uassert(6161505,
- "May not log out while using a security token based authentication",
- _authenticationMode != AuthenticationMode::kSecurityToken);
-
- if (!_authenticatedUser || (_authenticatedUser.value()->getName().getDB() != dbname)) {
- return;
+ bool isLoggedInOnDB =
+ (_authenticatedUser && _authenticatedUser.value()->getName().getDB() == dbname);
+ bool isExpiredOnDB = (_expiredUserName && _expiredUserName.value().getDB() == dbname);
+
+ if (isLoggedInOnDB || isExpiredOnDB) {
+ // The session either has an authenticated or expired user belonging to the database being
+ // logged out from. Calling logoutAllDatabases() will clear that user out.
+ logoutAllDatabases(client, reason);
}
-
- auto names = BSON_ARRAY(_authenticatedUser.value()->getName().toBSON());
- audit::logLogout(client, reason, names, BSONArray());
- _authenticatedUser = boost::none;
-
- clearImpersonatedUserData();
- _updateInternalAuthorizationState();
}
boost::optional<UserName> AuthorizationSessionImpl::getAuthenticatedUserName() {
@@ -374,7 +405,14 @@ void AuthorizationSessionImpl::grantInternalAuthorization(Client* client) {
return;
}
+ uassert(ErrorCodes::ReauthenticationRequired,
+ str::stream() << "Unable to grant internal authorization on an expired session, "
+ << "must reauthenticate as " << _expiredUserName->getUnambiguousName(),
+ _expiredUserName == boost::none);
+
_authenticatedUser = *internalSecurity.getUser();
+ _authenticationMode = AuthenticationMode::kConnection;
+ _expirationTime = boost::none;
_updateInternalAuthorizationState();
}
@@ -673,6 +711,7 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx)
stdx::lock_guard<Client> lk(*opCtx->getClient());
_authenticatedUser = boost::none;
_authenticationMode = AuthenticationMode::kNone;
+ _expirationTime = boost::none;
_updateInternalAuthorizationState();
};
@@ -1080,4 +1119,8 @@ bool AuthorizationSessionImpl::mayBypassWriteBlockingMode() const {
return MONGO_unlikely(_mayBypassWriteBlockingMode);
}
+bool AuthorizationSessionImpl::isExpired() const {
+ return _expiredUserName.has_value();
+}
+
} // namespace mongo
diff --git a/src/mongo/db/auth/authorization_session_impl.h b/src/mongo/db/auth/authorization_session_impl.h
index 94a13c59249..fc8a903ac59 100644
--- a/src/mongo/db/auth/authorization_session_impl.h
+++ b/src/mongo/db/auth/authorization_session_impl.h
@@ -76,7 +76,9 @@ public:
void startContractTracking() override;
- Status addAndAuthorizeUser(OperationContext* opCtx, const UserName& userName) override;
+ Status addAndAuthorizeUser(OperationContext* opCtx,
+ const UserName& userName,
+ boost::optional<Date_t> expirationTime) override;
User* lookupUser(const UserName& name) override;
@@ -158,6 +160,8 @@ public:
bool mayBypassWriteBlockingMode() const override;
+ bool isExpired() const override;
+
protected:
friend class AuthorizationSessionImplTestHelper;
@@ -171,7 +175,6 @@ protected:
// date.
void _updateInternalAuthorizationState();
-
// The User who has been authenticated on this connection.
boost::optional<UserHandle> _authenticatedUser;
@@ -221,5 +224,14 @@ private:
AuthorizationContract _contract;
bool _mayBypassWriteBlockingMode;
+
+ // The expiration time for this session, expressed as a Unix timestamp. After this time passes,
+ // the session will be expired and requests will fail until the expiration time is refreshed.
+ // If boost::none, then the session never expires (default behavior).
+ boost::optional<Date_t> _expirationTime;
+
+ // If the session is expired, this represents the UserName that was formerly authenticated on
+ // this connection.
+ boost::optional<UserName> _expiredUserName;
};
} // namespace mongo
diff --git a/src/mongo/db/auth/authorization_session_test.cpp b/src/mongo/db/auth/authorization_session_test.cpp
index 1336554897a..0241901ee41 100644
--- a/src/mongo/db/auth/authorization_session_test.cpp
+++ b/src/mongo/db/auth/authorization_session_test.cpp
@@ -44,16 +44,18 @@
#include "mongo/db/auth/authz_session_external_state_mock.h"
#include "mongo/db/auth/restriction_environment.h"
#include "mongo/db/auth/sasl_options.h"
+#include "mongo/db/auth/security_token_gen.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/json.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/pipeline/aggregation_request_helper.h"
-#include "mongo/db/service_context_test_fixture.h"
+#include "mongo/db/service_context_d_test_fixture.h"
#include "mongo/idl/server_parameter_test_util.h"
#include "mongo/transport/session.h"
#include "mongo/transport/transport_layer_mock.h"
#include "mongo/unittest/unittest.h"
+#include "mongo/util/clock_source_mock.h"
namespace mongo {
namespace {
@@ -82,9 +84,11 @@ private:
bool _findsShouldFail{false};
};
-class AuthorizationSessionTest : public ScopedGlobalServiceContextForTest, public unittest::Test {
+class AuthorizationSessionTest : public ServiceContextMongoDTest {
public:
void setUp() {
+ ServiceContextMongoDTest::setUp();
+
_session = transportLayer.createSession();
_client = getServiceContext()->makeClient("testClient", _session);
RestrictionEnvironment::set(
@@ -103,6 +107,7 @@ public:
std::move(localSessionState),
AuthorizationSessionImpl::InstallMockForTestingOrAuthImpl{});
authzManager->setAuthEnabled(true);
+ authzSession->startContractTracking();
credentials =
BSON("SCRAM-SHA-1" << scram::Secrets<SHA1Block>::generateCredentials(
@@ -114,6 +119,7 @@ public:
void tearDown() override {
authzSession->logoutAllDatabases(_client.get(), "Ending AuthorizationSessionTest");
+ ServiceContextMongoDTest::tearDown();
}
Status createUser(const UserName& username, const std::vector<RoleName>& roles) {
@@ -128,7 +134,55 @@ public:
}
rolesBSON.doneFast();
- return managerState->insertPrivilegeDocument(_opCtx.get(), userDoc.obj(), {});
+ return managerState->insert(_opCtx.get(),
+ NamespaceString(username.getTenant(),
+ NamespaceString::kAdminDb,
+ NamespaceString::kSystemUsers),
+ userDoc.obj(),
+ {});
+ }
+
+ void assertLogout(const ResourcePattern& resource, ActionType action) {
+ ASSERT_FALSE(authzSession->isExpired());
+ ASSERT_EQ(authzSession->getAuthenticationMode(),
+ AuthorizationSession::AuthenticationMode::kNone);
+ ASSERT_FALSE(authzSession->isAuthenticated());
+ ASSERT_EQ(authzSession->getAuthenticatedUser(), boost::none);
+ ASSERT_FALSE(authzSession->isAuthorizedForActionsOnResource(resource, action));
+ }
+
+ void assertExpired(const ResourcePattern& resource, ActionType action) {
+ ASSERT_TRUE(authzSession->isExpired());
+ ASSERT_EQ(authzSession->getAuthenticationMode(),
+ AuthorizationSession::AuthenticationMode::kNone);
+ ASSERT_FALSE(authzSession->isAuthenticated());
+ ASSERT_EQ(authzSession->getAuthenticatedUser(), boost::none);
+ ASSERT_FALSE(authzSession->isAuthorizedForActionsOnResource(resource, action));
+ }
+
+ void assertActive(const ResourcePattern& resource, ActionType action) {
+ ASSERT_FALSE(authzSession->isExpired());
+ ASSERT_EQ(authzSession->getAuthenticationMode(),
+ AuthorizationSession::AuthenticationMode::kConnection);
+ ASSERT_TRUE(authzSession->isAuthenticated());
+ ASSERT_NOT_EQUALS(authzSession->getAuthenticatedUser(), boost::none);
+ ASSERT_TRUE(authzSession->isAuthorizedForActionsOnResource(resource, action));
+ }
+
+ void assertSecurityToken(const ResourcePattern& resource, ActionType action) {
+ ASSERT_FALSE(authzSession->isExpired());
+ ASSERT_EQ(authzSession->getAuthenticationMode(),
+ AuthorizationSession::AuthenticationMode::kSecurityToken);
+ ASSERT_TRUE(authzSession->isAuthenticated());
+ ASSERT_NOT_EQUALS(authzSession->getAuthenticatedUser(), boost::none);
+ ASSERT_TRUE(authzSession->isAuthorizedForActionsOnResource(resource, action));
+ }
+
+protected:
+ AuthorizationSessionTest() : ServiceContextMongoDTest(Options{}.useMockClock(true)) {}
+
+ ClockSourceMock* clockSource() {
+ return static_cast<ClockSourceMock*>(getServiceContext()->getFastClockSource());
}
protected:
@@ -173,39 +227,31 @@ const ResourcePattern thirdProfileCollResource(
ResourcePattern::forExactNamespace(NamespaceString("third.system.profile")));
TEST_F(AuthorizationSessionTest, MultiAuthSameUserAllowed) {
- authzSession->startContractTracking();
-
ASSERT_OK(createUser({"user1", "test"}, {}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}, boost::none));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}, boost::none));
authzSession->logoutAllDatabases(_client.get(), "Test finished");
}
TEST_F(AuthorizationSessionTest, MultiAuthSameDBDisallowed) {
- authzSession->startContractTracking();
-
ASSERT_OK(createUser({"user1", "test"}, {}));
ASSERT_OK(createUser({"user2", "test"}, {}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}));
- ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user2", "test"}));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user1", "test"}, boost::none));
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user2", "test"}, boost::none));
authzSession->logoutAllDatabases(_client.get(), "Test finished");
}
TEST_F(AuthorizationSessionTest, MultiAuthMultiDBDisallowed) {
- authzSession->startContractTracking();
-
ASSERT_OK(createUser({"user", "test1"}, {}));
ASSERT_OK(createUser({"user", "test2"}, {}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user", "test1"}));
- ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user", "test2"}));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user", "test1"}, boost::none));
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), {"user", "test2"}, boost::none));
authzSession->logoutAllDatabases(_client.get(), "Test finished");
}
TEST_F(AuthorizationSessionTest, AddUserAndCheckAuthorization) {
- authzSession->startContractTracking();
-
// Check that disabling auth checks works
ASSERT_FALSE(
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::insert));
@@ -217,12 +263,14 @@ TEST_F(AuthorizationSessionTest, AddUserAndCheckAuthorization) {
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::insert));
// Check that you can't authorize a user that doesn't exist.
- ASSERT_EQUALS(ErrorCodes::UserNotFound,
- authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_EQUALS(
+ ErrorCodes::UserNotFound,
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
// Add a user with readWrite and dbAdmin on the test DB
ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}, {"dbAdmin", "test"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
ASSERT_TRUE(
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::insert));
@@ -234,7 +282,8 @@ TEST_F(AuthorizationSessionTest, AddUserAndCheckAuthorization) {
// Add an admin user with readWriteAnyDatabase
ASSERT_OK(createUser({"admin", "admin"}, {{"readWriteAnyDatabase", "admin"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("admin", "admin")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("admin", "admin"), boost::none));
ASSERT_TRUE(authzSession->isAuthorizedForActionsOnResource(
ResourcePattern::forExactNamespace(NamespaceString("anydb.somecollection")),
@@ -288,7 +337,8 @@ TEST_F(AuthorizationSessionTest, DuplicateRolesOK) {
// Add a user with doubled-up readWrite and single dbAdmin on the test DB
ASSERT_OK(createUser({"spencer", "test"},
{{"readWrite", "test"}, {"dbAdmin", "test"}, {"readWrite", "test"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
ASSERT_TRUE(
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::insert));
@@ -306,7 +356,8 @@ TEST_F(AuthorizationSessionTest, SystemCollectionsAccessControl) {
{{"readWriteAnyDatabase", "admin"}, {"dbAdminAnyDatabase", "admin"}}));
ASSERT_OK(createUser({"useradminany", "test"}, {{"userAdminAnyDatabase", "admin"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("rwany", "test")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("rwany", "test"), boost::none));
ASSERT_FALSE(
authzSession->isAuthorizedForActionsOnResource(testUsersCollResource, ActionType::insert));
@@ -322,7 +373,8 @@ TEST_F(AuthorizationSessionTest, SystemCollectionsAccessControl) {
authzSession->isAuthorizedForActionsOnResource(otherProfileCollResource, ActionType::find));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("useradminany", "test")));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("useradminany", "test"), boost::none));
ASSERT_FALSE(
authzSession->isAuthorizedForActionsOnResource(testUsersCollResource, ActionType::insert));
ASSERT_TRUE(
@@ -337,7 +389,7 @@ TEST_F(AuthorizationSessionTest, SystemCollectionsAccessControl) {
authzSession->isAuthorizedForActionsOnResource(otherProfileCollResource, ActionType::find));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("rw", "test")));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("rw", "test"), boost::none));
ASSERT_FALSE(
authzSession->isAuthorizedForActionsOnResource(testUsersCollResource, ActionType::insert));
@@ -353,7 +405,8 @@ TEST_F(AuthorizationSessionTest, SystemCollectionsAccessControl) {
authzSession->isAuthorizedForActionsOnResource(otherProfileCollResource, ActionType::find));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("useradmin", "test")));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("useradmin", "test"), boost::none));
ASSERT_FALSE(
authzSession->isAuthorizedForActionsOnResource(testUsersCollResource, ActionType::insert));
ASSERT_FALSE(
@@ -372,7 +425,8 @@ TEST_F(AuthorizationSessionTest, SystemCollectionsAccessControl) {
TEST_F(AuthorizationSessionTest, InvalidateUser) {
// Add a readWrite user
ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
ASSERT_TRUE(
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::find));
@@ -420,7 +474,8 @@ TEST_F(AuthorizationSessionTest, InvalidateUser) {
TEST_F(AuthorizationSessionTest, UseOldUserInfoInFaceOfConnectivityProblems) {
// Add a readWrite user
ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
ASSERT_TRUE(
authzSession->isAuthorizedForActionsOnResource(testFooCollResource, ActionType::find));
@@ -491,7 +546,8 @@ TEST_F(AuthorizationSessionTest, AcquireUserObtainsAndValidatesAuthenticationRes
std::make_unique<RestrictionEnvironment>(
SockAddr::create(clientSource, 5555, AF_UNSPEC),
SockAddr::create(serverAddress, 27017, AF_UNSPEC)));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("spencer", "test"), boost::none));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
};
@@ -500,11 +556,13 @@ TEST_F(AuthorizationSessionTest, AcquireUserObtainsAndValidatesAuthenticationRes
std::make_unique<RestrictionEnvironment>(
SockAddr::create(clientSource, 5555, AF_UNSPEC),
SockAddr::create(serverAddress, 27017, AF_UNSPEC)));
- ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("spencer", "test"), boost::none));
};
// The empty RestrictionEnvironment will cause addAndAuthorizeUser to fail.
- ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
+ ASSERT_NOT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
// A clientSource from the 192.168.0.0/24 block will succeed in connecting to a server
// listening on 192.168.0.2.
@@ -1070,7 +1128,7 @@ TEST_F(AuthorizationSessionTest, UnauthorizedSessionIsCoauthorizedWithAnybodyWhe
TEST_F(AuthorizationSessionTest, AuthorizedSessionIsNotCoauthorizedNobody) {
UserName user("spencer", "test");
ASSERT_OK(createUser(user, {}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user, boost::none));
ASSERT_FALSE(authzSession->isCoauthorizedWith(boost::none));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
}
@@ -1079,7 +1137,7 @@ TEST_F(AuthorizationSessionTest, AuthorizedSessionIsCoauthorizedNobodyWhenAuthIs
UserName user("spencer", "test");
authzManager->setAuthEnabled(false);
ASSERT_OK(createUser(user, {}));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user, boost::none));
ASSERT_TRUE(authzSession->isCoauthorizedWith(user));
authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
}
@@ -1163,8 +1221,6 @@ TEST_F(AuthorizationSessionTest, CanUseUUIDNamespacesWithPrivilege) {
BSONObj uuidObj = BSON("a" << UUID::gen());
BSONObj invalidObj = BSON("a" << 12);
- authzSession->startContractTracking();
-
// Strings require no privileges
ASSERT_TRUE(authzSession->isAuthorizedToParseNamespaceElement(stringObj.firstElement()));
@@ -1196,6 +1252,195 @@ TEST_F(AuthorizationSessionTest, CanUseUUIDNamespacesWithPrivilege) {
authzSession->verifyContract(&ac);
}
+TEST_F(AuthorizationSessionTest, MayBypassWriteBlockingModeIsSetCorrectly) {
+ ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
+
+ // Add a user without the restore role and ensure we can't bypass
+ ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
+ BSON("user"
+ << "spencer"
+ << "db"
+ << "test"
+ << "credentials" << credentials << "roles"
+ << BSON_ARRAY(BSON("role"
+ << "readWrite"
+ << "db"
+ << "test"))),
+ BSONObj()));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
+ ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
+
+ // Add a user with restore role on admin db and ensure we can bypass
+ ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
+ BSON("user"
+ << "gmarks"
+ << "db"
+ << "admin"
+ << "credentials" << credentials << "roles"
+ << BSON_ARRAY(BSON("role"
+ << "restore"
+ << "db"
+ << "admin"))),
+ BSONObj()));
+ authzSession->logoutDatabase(_client.get(), "test", "End of test");
+
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("gmarks", "admin"), boost::none));
+ ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
+
+ // Remove that user by logging out of the admin db and ensure we can't bypass anymore
+ authzSession->logoutDatabase(_client.get(), "admin", "");
+ ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
+
+ // Add a user with the root role, which should confer restore role for cluster resource, and
+ // ensure we can bypass
+ ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
+ BSON("user"
+ << "admin"
+ << "db"
+ << "admin"
+ << "credentials" << credentials << "roles"
+ << BSON_ARRAY(BSON("role"
+ << "root"
+ << "db"
+ << "admin"))),
+ BSONObj()));
+ authzSession->logoutDatabase(_client.get(), "admin", "");
+
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("admin", "admin"), boost::none));
+ ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
+
+ // Remove non-privileged user by logging out of test db and ensure we can still bypass
+ authzSession->logoutDatabase(_client.get(), "test", "");
+ ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
+
+ // Remove privileged user by logging out of admin db and ensure we cannot bypass
+ authzSession->logoutDatabase(_client.get(), "admin", "");
+ ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
+}
+
+TEST_F(AuthorizationSessionTest, InvalidExpirationTime) {
+ // Create and authorize valid user with invalid expiration.
+ Date_t expirationTime = clockSource()->now() - Hours(1);
+ ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}, {"dbAdmin", "test"}}));
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("spencer", "test"), expirationTime));
+}
+
+TEST_F(AuthorizationSessionTest, NoExpirationTime) {
+ // Create and authorize valid user with no expiration.
+ ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}, {"dbAdmin", "test"}}));
+ ASSERT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test"), boost::none));
+ assertActive(testFooCollResource, ActionType::insert);
+
+ // Assert that moving the clock forward has no impact on a session without expiration time.
+ clockSource()->advance(Hours(24));
+ authzSession->startRequest(_opCtx.get());
+ assertActive(testFooCollResource, ActionType::insert);
+
+ // Assert that logout occurs normally.
+ authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
+ assertLogout(testFooCollResource, ActionType::insert);
+}
+
+TEST_F(AuthorizationSessionTest, ExpiredSessionWithReauth) {
+ // Tests authorization session flow from unauthenticated to active to expired to active (reauth)
+ // to expired to logged out.
+
+ // Create and authorize a user with a valid expiration time set in the future.
+ Date_t expirationTime = clockSource()->now() + Hours(1);
+ ASSERT_OK(createUser({"spencer", "test"}, {{"readWrite", "test"}, {"dbAdmin", "test"}}));
+ ASSERT_OK(createUser({"admin", "admin"}, {{"readWriteAnyDatabase", "admin"}}));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("spencer", "test"), expirationTime));
+
+ // Assert that advancing the clock by 30 minutes does not trigger expiration.
+ auto clock = clockSource();
+ clock->advance(Minutes(30));
+ authzSession->startRequest(
+ _opCtx.get()); // Refreshes session's authentication state based on expiration.
+ assertActive(testFooCollResource, ActionType::insert);
+
+ // Assert that the session is now expired and subsequently is no longer authenticated or
+ // authorized to do anything after fast-forwarding the clock source.
+ clock->advance(Hours(2));
+ authzSession->startRequest(
+ _opCtx.get()); // Refreshes session's authentication state based on expiration.
+ assertExpired(testFooCollResource, ActionType::insert);
+
+ // Authorize the same user again to simulate re-login.
+ expirationTime += Hours(2);
+ ASSERT_OK(authzSession->addAndAuthorizeUser(
+ _opCtx.get(), UserName("spencer", "test"), expirationTime));
+ assertActive(testFooCollResource, ActionType::insert);
+
+ // Expire the user again, this time by setting clock to the exact expiration time boundary.
+ clock->reset(expirationTime);
+ authzSession->startRequest(_opCtx.get());
+ assertExpired(testFooCollResource, ActionType::insert);
+
+ // Assert that a different user cannot log in on the expired connection.
+ ASSERT_NOT_OK(
+ authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("admin", "admin"), boost::none));
+ assertExpired(testFooCollResource, ActionType::insert);
+
+ // Check that explicit logout from an expired connection works as expected.
+ authzSession->logoutDatabase(_client.get(), "test", "Kill the test!");
+ assertLogout(ResourcePattern::forExactNamespace(NamespaceString("anydb.somecollection")),
+ ActionType::insert);
+}
+
+TEST_F(AuthorizationSessionTest, ExpirationWithSecurityTokenNOK) {
+ // Tests authorization flow from unauthenticated to active (via token) to unauthenticated to
+ // active (via stateful connection) to unauthenticated.
+ using VTS = auth::ValidatedTenancyScope;
+
+ // Create and authorize a security token user.
+ constexpr auto authUserFieldName = auth::SecurityToken::kAuthenticatedUserFieldName;
+ auto kOid = OID::gen();
+ auto body = BSON("ping" << 1 << "$tenant" << kOid);
+ UserName user("spencer", "test", TenantId(kOid));
+ UserName adminUser("admin", "admin");
+
+ ASSERT_OK(createUser(user, {{"readWrite", "test"}, {"dbAdmin", "test"}}));
+ ASSERT_OK(createUser(adminUser, {{"readWriteAnyDatabase", "admin"}}));
+
+ VTS validatedTenancyScope = VTS(BSON(authUserFieldName << user.toBSON(true /* encodeTenant */)),
+ VTS::TokenForTestingTag{});
+ VTS::set(_opCtx.get(), validatedTenancyScope);
+
+ // Make sure that security token users can't be authorized with an expiration date.
+ Date_t expirationTime = clockSource()->now() + Hours(1);
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user, expirationTime));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), user, boost::none));
+
+ // Assert that the session is authenticated and authorized as expected.
+ assertSecurityToken(testFooCollResource, ActionType::insert);
+
+ // Assert that another user can't be authorized while the security token is auth'd.
+ ASSERT_NOT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), adminUser, boost::none));
+
+ // Check that starting a new request without the security token decoration results in token user
+ // logout.
+ VTS::set(_opCtx.get(), boost::none);
+ authzSession->startRequest(_opCtx.get());
+ assertLogout(testFooCollResource, ActionType::insert);
+
+ // Assert that a connection-based user with an expiration policy can be authorized after token
+ // logout.
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), adminUser, expirationTime));
+ assertActive(ResourcePattern::forExactNamespace(NamespaceString("anydb.somecollection")),
+ ActionType::insert);
+
+ // Check that logout proceeds normally.
+ authzSession->logoutDatabase(_client.get(), "admin", "Kill the test!");
+ assertLogout(ResourcePattern::forExactNamespace(NamespaceString("anydb.somecollection")),
+ ActionType::insert);
+}
+
class SystemBucketsTest : public AuthorizationSessionTest {
protected:
static constexpr auto sb_db_test = "sb_db_test"_sd;
@@ -1448,71 +1693,5 @@ TEST_F(SystemBucketsTest, CanCheckIfHasAnyPrivilegeInResourceDBForSystemBuckets)
ASSERT_TRUE(authzSession->isAuthorizedForAnyActionOnAnyResourceInDB(sb_db_other));
}
-TEST_F(AuthorizationSessionTest, MayBypassWriteBlockingModeIsSetCorrectly) {
- ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
-
- // Add a user without the restore role and ensure we can't bypass
- ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
- BSON("user"
- << "spencer"
- << "db"
- << "test"
- << "credentials" << credentials << "roles"
- << BSON_ARRAY(BSON("role"
- << "readWrite"
- << "db"
- << "test"))),
- BSONObj()));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("spencer", "test")));
- ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
-
- // Add a user with restore role on admin db and ensure we can bypass
- ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
- BSON("user"
- << "gmarks"
- << "db"
- << "admin"
- << "credentials" << credentials << "roles"
- << BSON_ARRAY(BSON("role"
- << "restore"
- << "db"
- << "admin"))),
- BSONObj()));
- authzSession->logoutDatabase(_client.get(), "test", "End of test");
-
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("gmarks", "admin")));
- ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
-
- // Remove that user by logging out of the admin db and ensure we can't bypass anymore
- authzSession->logoutDatabase(_client.get(), "admin", "");
- ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
-
- // Add a user with the root role, which should confer restore role for cluster resource, and
- // ensure we can bypass
- ASSERT_OK(managerState->insertPrivilegeDocument(_opCtx.get(),
- BSON("user"
- << "admin"
- << "db"
- << "admin"
- << "credentials" << credentials << "roles"
- << BSON_ARRAY(BSON("role"
- << "root"
- << "db"
- << "admin"))),
- BSONObj()));
- authzSession->logoutDatabase(_client.get(), "admin", "");
-
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), UserName("admin", "admin")));
- ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
-
- // Remove non-privileged user by logging out of test db and ensure we can still bypass
- authzSession->logoutDatabase(_client.get(), "test", "");
- ASSERT_TRUE(authzSession->mayBypassWriteBlockingMode());
-
- // Remove privileged user by logging out of admin db and ensure we cannot bypass
- authzSession->logoutDatabase(_client.get(), "admin", "");
- ASSERT_FALSE(authzSession->mayBypassWriteBlockingMode());
-}
-
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/auth/sasl_commands.cpp b/src/mongo/db/auth/sasl_commands.cpp
index fb310d7d006..5a50334a6ce 100644
--- a/src/mongo/db/auth/sasl_commands.cpp
+++ b/src/mongo/db/auth/sasl_commands.cpp
@@ -206,8 +206,9 @@ SaslReply doSaslStep(OperationContext* opCtx,
if (mechanism.isSuccess()) {
UserName userName(mechanism.getPrincipalName(), mechanism.getAuthenticationDatabase());
- uassertStatusOK(
- AuthorizationSession::get(opCtx->getClient())->addAndAuthorizeUser(opCtx, userName));
+ auto expirationTime = mechanism.getExpirationTime();
+ uassertStatusOK(AuthorizationSession::get(opCtx->getClient())
+ ->addAndAuthorizeUser(opCtx, userName, expirationTime));
if (!serverGlobalParams.quiet.load()) {
auto attrs = makeLogAttributes();
diff --git a/src/mongo/db/auth/sasl_mechanism_registry.h b/src/mongo/db/auth/sasl_mechanism_registry.h
index 225c9d26595..f2011909368 100644
--- a/src/mongo/db/auth/sasl_mechanism_registry.h
+++ b/src/mongo/db/auth/sasl_mechanism_registry.h
@@ -147,6 +147,15 @@ public:
}
/**
+ * Returns the expiration time, if applicable, of the user's authentication for the given
+ * mechanism. The default of boost::none indicates that the user will be authenticated
+ * indefinitely on the session.
+ */
+ virtual boost::optional<Date_t> getExpirationTime() const {
+ return boost::none;
+ }
+
+ /**
* Appends mechanism specific info in BSON form. The schema of this BSON will vary by mechanism
* implementation, thus this info is entirely diagnostic/for records.
*/
diff --git a/src/mongo/db/auth/security_token_authentication_guard.cpp b/src/mongo/db/auth/security_token_authentication_guard.cpp
index 5be6de3dc75..a989a356089 100644
--- a/src/mongo/db/auth/security_token_authentication_guard.cpp
+++ b/src/mongo/db/auth/security_token_authentication_guard.cpp
@@ -43,7 +43,8 @@ SecurityTokenAuthenticationGuard::SecurityTokenAuthenticationGuard(
if (token.hasAuthenticatedUser()) {
const auto& userName = token.authenticatedUser();
auto* client = opCtx->getClient();
- uassertStatusOK(AuthorizationSession::get(client)->addAndAuthorizeUser(opCtx, userName));
+ uassertStatusOK(
+ AuthorizationSession::get(client)->addAndAuthorizeUser(opCtx, userName, boost::none));
_client = client;
LOGV2_DEBUG(5838100,
diff --git a/src/mongo/db/commands.cpp b/src/mongo/db/commands.cpp
index f83ac353a23..0f9721a50b9 100644
--- a/src/mongo/db/commands.cpp
+++ b/src/mongo/db/commands.cpp
@@ -84,11 +84,18 @@ bool checkAuthorizationImplPreParse(OperationContext* opCtx,
auto client = opCtx->getClient();
if (client->isInDirectClient())
return true;
+
uassert(ErrorCodes::Unauthorized,
str::stream() << command->getName() << " may only be run against the admin database.",
!command->adminOnly() || request.getDatabase() == NamespaceString::kAdminDb);
auto authzSession = AuthorizationSession::get(client);
+ uassert(ErrorCodes::ReauthenticationRequired,
+ fmt::format("Command {} requires reauthentication since the current authorization "
+ "session has expired. Please re-auth.",
+ command->getName()),
+ !command->requiresAuth() || !authzSession->isExpired());
+
if (!authzSession->getAuthorizationManager().isAuthEnabled()) {
// Running without auth, so everything should be allowed except remotely invoked
// commands that have the 'localHostOnlyIfNoAuth' restriction.
@@ -99,13 +106,16 @@ bool checkAuthorizationImplPreParse(OperationContext* opCtx,
client->getIsLocalHostConnection());
return true; // Blanket authorization: don't need to check anything else.
}
+
if (authzSession->isUsingLocalhostBypass())
return false; // Still can't decide on auth because of the localhost bypass.
+
uassert(ErrorCodes::Unauthorized,
- str::stream() << "command " << command->getName() << " requires authentication",
+ str::stream() << "Command " << command->getName() << " requires authentication",
!command->requiresAuth() || authzSession->isAuthenticated() ||
(request.validatedTenancyScope &&
request.validatedTenancyScope->hasAuthenticatedUser()));
+
return false;
}
diff --git a/src/mongo/db/commands/authentication_commands.cpp b/src/mongo/db/commands/authentication_commands.cpp
index f3a0107bbec..cb99591884e 100644
--- a/src/mongo/db/commands/authentication_commands.cpp
+++ b/src/mongo/db/commands/authentication_commands.cpp
@@ -198,7 +198,7 @@ void _authenticateX509(OperationContext* opCtx, AuthenticationSession* session)
uassert(ErrorCodes::BadValue,
kX509AuthenticationDisabledMessage,
!isX509AuthDisabled(opCtx->getServiceContext()));
- uassertStatusOK(authorizationSession->addAndAuthorizeUser(opCtx, user));
+ uassertStatusOK(authorizationSession->addAndAuthorizeUser(opCtx, user, boost::none));
};
if (sslConfiguration->isClusterMember(clientName)) {
diff --git a/src/mongo/db/commands_test.cpp b/src/mongo/db/commands_test.cpp
index 0c994f61d01..40dff290ee1 100644
--- a/src/mongo/db/commands_test.cpp
+++ b/src/mongo/db/commands_test.cpp
@@ -29,15 +29,24 @@
#include "mongo/platform/basic.h"
+#include "mongo/crypto/mechanism_scram.h"
+#include "mongo/db/auth/authorization_manager.h"
+#include "mongo/db/auth/authorization_session_for_test.h"
+#include "mongo/db/auth/authz_manager_external_state_mock.h"
+#include "mongo/db/auth/authz_session_external_state_mock.h"
+#include "mongo/db/auth/sasl_options.h"
#include "mongo/db/catalog/collection_mock.h"
#include "mongo/db/commands.h"
#include "mongo/db/commands_test_example_gen.h"
#include "mongo/db/dbmessage.h"
-#include "mongo/db/service_context_test_fixture.h"
+#include "mongo/db/service_context_d_test_fixture.h"
#include "mongo/rpc/factory.h"
#include "mongo/rpc/op_msg_rpc_impls.h"
+#include "mongo/transport/session.h"
+#include "mongo/transport/transport_layer_mock.h"
#include "mongo/unittest/death_test.h"
#include "mongo/unittest/unittest.h"
+#include "mongo/util/clock_source_mock.h"
namespace mongo {
namespace {
@@ -307,8 +316,8 @@ public:
mutable std::int32_t iCapture = 0;
};
-template <typename Fn>
-class MyCommand final : public TypedCommand<MyCommand<Fn>> {
+template <typename Fn, typename AuthFn>
+class MyCommand final : public TypedCommand<MyCommand<Fn, AuthFn>> {
public:
class Invocation final : public TypedCommand<MyCommand>::InvocationBase {
public:
@@ -326,7 +335,9 @@ public:
bool supportsWriteConcern() const override {
return false;
}
- void doCheckAuthorization(OperationContext* opCtx) const override {}
+ void doCheckAuthorization(OperationContext* opCtx) const override {
+ return _command()->_authFn();
+ }
const MyCommand* _command() const {
return static_cast<const MyCommand*>(Base::definition());
@@ -335,7 +346,10 @@ public:
using Request = commands_test_example::ExampleVoid;
- MyCommand(StringData name, Fn fn) : TypedCommand<MyCommand<Fn>>(name), _fn{std::move(fn)} {}
+ MyCommand(StringData name, Fn fn, AuthFn authFn)
+ : TypedCommand<MyCommand<Fn, AuthFn>>(name),
+ _fn{std::move(fn)},
+ _authFn{std::move(authFn)} {}
private:
Command::AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
@@ -347,30 +361,90 @@ private:
}
Fn _fn;
+ AuthFn _authFn;
};
-template <typename Fn>
-using CmdT = MyCommand<typename std::decay<Fn>::type>;
+template <typename Fn, typename AuthFn>
+using CmdT = MyCommand<typename std::decay<Fn>::type, typename std::decay<AuthFn>::type>;
auto throwFn = [] { uasserted(ErrorCodes::UnknownError, "some error"); };
+auto authSuccessFn = [] { return; };
+auto authFailFn = [] { uasserted(ErrorCodes::Unauthorized, "Not authorized"); };
ExampleIncrementCommand exampleIncrementCommand;
ExampleMinimalCommand exampleMinimalCommand;
ExampleVoidCommand exampleVoidCommand;
-CmdT<decltype(throwFn)> throwStatusCommand("throwsStatus", throwFn);
+CmdT<decltype(throwFn), decltype(authSuccessFn)> throwStatusCommand("throwsStatus",
+ throwFn,
+ authSuccessFn);
+CmdT<decltype(throwFn), decltype(authFailFn)> unauthorizedCommand("unauthorizedCmd",
+ throwFn,
+ authFailFn);
+
+class TypedCommandTest : public ServiceContextMongoDTest {
+public:
+ void setUp() {
+ ServiceContextMongoDTest::setUp();
+
+ // Set up the auth subsystem to authorize the command.
+ auto localManagerState = std::make_unique<AuthzManagerExternalStateMock>();
+ _managerState = localManagerState.get();
+ _managerState->setAuthzVersion(AuthorizationManager::schemaVersion26Final);
+ auto uniqueAuthzManager = std::make_unique<AuthorizationManagerImpl>(
+ getServiceContext(), std::move(localManagerState));
+ _authzManager = uniqueAuthzManager.get();
+ AuthorizationManager::set(getServiceContext(), std::move(uniqueAuthzManager));
+ _authzManager->setAuthEnabled(true);
+
+ _session = _transportLayer.createSession();
+ _client = getServiceContext()->makeClient("testClient", _session);
+ RestrictionEnvironment::set(
+ _session, std::make_unique<RestrictionEnvironment>(SockAddr(), SockAddr()));
+ _authzSession = AuthorizationSession::get(_client.get());
+
+ // Insert a user document that will represent the user used for running the commands.
+ auto credentials =
+ BSON("SCRAM-SHA-1" << scram::Secrets<SHA1Block>::generateCredentials(
+ "a", saslGlobalParams.scramSHA1IterationCount.load())
+ << "SCRAM-SHA-256"
+ << scram::Secrets<SHA256Block>::generateCredentials(
+ "a", saslGlobalParams.scramSHA256IterationCount.load()));
+
+ BSONObj userDoc = BSON("_id"_sd
+ << "test.varun"_sd
+ << "user"_sd
+ << "varun"
+ << "db"_sd
+ << "test"
+ << "credentials"_sd << credentials << "roles"_sd
+ << BSON_ARRAY(BSON("role"_sd
+ << "readWrite"_sd
+ << "db"_sd
+ << "test"_sd)));
+
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_managerState->insertPrivilegeDocument(opCtx.get(), userDoc, {}));
+ }
-class TypedCommandTest : public ServiceContextTest {
protected:
+ TypedCommandTest() : ServiceContextMongoDTest(Options{}.useMockClock(true)) {}
+
+ ClockSourceMock* clockSource() {
+ return static_cast<ClockSourceMock*>(getServiceContext()->getFastClockSource());
+ }
+
template <typename T>
void runIncr(T& command, std::function<void(int, const BSONObj&)> postAssert) {
const NamespaceString ns("testdb.coll");
+
for (std::int32_t i : {123, 12345, 0, -456}) {
const OpMsgRequest request = [&] {
typename T::Request incr(ns);
incr.setI(i);
return incr.serialize(BSON("$db" << ns.db()));
}();
- auto opCtx = makeOperationContext();
+
+ auto opCtx = _client->makeOperationContext();
auto invocation = command.parse(opCtx.get(), request);
ASSERT_EQ(invocation->ns(), ns);
@@ -378,6 +452,7 @@ protected:
const BSONObj reply = [&] {
rpc::OpMsgReplyBuilder replyBuilder;
try {
+ invocation->checkAuthorization(opCtx.get(), request);
invocation->run(opCtx.get(), &replyBuilder);
auto bob = replyBuilder.getBodyBuilder();
CommandHelpers::extractOrAppendOk(bob);
@@ -391,9 +466,23 @@ protected:
postAssert(i, reply);
}
}
+
+protected:
+ AuthorizationManager* _authzManager;
+ AuthzManagerExternalStateMock* _managerState;
+ transport::TransportLayerMock _transportLayer;
+ transport::SessionHandle _session;
+ ServiceContext::UniqueClient _client;
+ AuthorizationSession* _authzSession;
};
TEST_F(TypedCommandTest, runTyped) {
+ {
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, boost::none));
+ _authzSession->startRequest(opCtx.get());
+ }
+
runIncr(exampleIncrementCommand, [](int i, const BSONObj& reply) {
ASSERT_EQ(reply["ok"].Double(), 1.0);
ASSERT_EQ(reply["iPlusOne"].Int(), i + 1);
@@ -401,6 +490,12 @@ TEST_F(TypedCommandTest, runTyped) {
}
TEST_F(TypedCommandTest, runMinimal) {
+ {
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, boost::none));
+ _authzSession->startRequest(opCtx.get());
+ }
+
runIncr(exampleMinimalCommand, [](int i, const BSONObj& reply) {
ASSERT_EQ(reply["ok"].Double(), 1.0);
ASSERT_EQ(reply["iPlusOne"].Int(), i + 1);
@@ -408,6 +503,12 @@ TEST_F(TypedCommandTest, runMinimal) {
}
TEST_F(TypedCommandTest, runVoid) {
+ {
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, boost::none));
+ _authzSession->startRequest(opCtx.get());
+ }
+
runIncr(exampleVoidCommand, [](int i, const BSONObj& reply) {
ASSERT_EQ(reply["ok"].Double(), 1.0);
ASSERT_EQ(exampleVoidCommand.iCapture, i + 1);
@@ -415,6 +516,12 @@ TEST_F(TypedCommandTest, runVoid) {
}
TEST_F(TypedCommandTest, runThrowStatus) {
+ {
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, boost::none));
+ _authzSession->startRequest(opCtx.get());
+ }
+
runIncr(throwStatusCommand, [](int i, const BSONObj& reply) {
Status status = Status::OK();
try {
@@ -429,5 +536,66 @@ TEST_F(TypedCommandTest, runThrowStatus) {
});
}
+TEST_F(TypedCommandTest, runThrowDoCheckAuthorization) {
+ {
+ auto opCtx = _client->makeOperationContext();
+ ASSERT_OK(_authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, boost::none));
+ _authzSession->startRequest(opCtx.get());
+ }
+
+ runIncr(unauthorizedCommand, [](int i, const BSONObj& reply) {
+ Status status = Status::OK();
+ try {
+ (void)authFailFn();
+ } catch (const DBException& e) {
+ status = e.toStatus();
+ }
+ ASSERT_EQ(reply["ok"].Double(), 0.0);
+ ASSERT_EQ(reply["code"].Int(), status.code());
+ ASSERT_EQ(reply["codeName"].String(), ErrorCodes::errorString(status.code()));
+ });
+}
+
+TEST_F(TypedCommandTest, runThrowNoUserAuthenticated) {
+ {
+ // Don't authenticate any users.
+ auto opCtx = _client->makeOperationContext();
+ _authzSession->startRequest(opCtx.get());
+ }
+
+ runIncr(exampleIncrementCommand, [](int i, const BSONObj& reply) {
+ ASSERT_EQ(reply["ok"].Double(), 0.0);
+ ASSERT_EQ(reply["errmsg"].String(),
+ str::stream() << "Command exampleIncrement requires authentication");
+ ASSERT_EQ(reply["code"].Int(), ErrorCodes::Unauthorized);
+ ASSERT_EQ(reply["codeName"].String(), ErrorCodes::errorString(ErrorCodes::Unauthorized));
+ });
+}
+
+TEST_F(TypedCommandTest, runThrowAuthzSessionExpired) {
+ {
+ // Load user into the authorization session and then expire it.
+ auto opCtx = _client->makeOperationContext();
+ auto expirationTime = clockSource()->now() + Hours{1};
+ ASSERT_OK(
+ _authzSession->addAndAuthorizeUser(opCtx.get(), {"varun", "test"}, expirationTime));
+
+ // Fast-forward time before starting a new request.
+ clockSource()->advance(Hours(2));
+ _authzSession->startRequest(opCtx.get());
+ }
+
+ runIncr(exampleIncrementCommand, [](int i, const BSONObj& reply) {
+ ASSERT_EQ(reply["ok"].Double(), 0.0);
+ ASSERT_EQ(
+ reply["errmsg"].String(),
+ str::stream() << "Command exampleIncrement requires reauthentication since the current "
+ "authorization session has expired. Please re-auth.");
+ ASSERT_EQ(reply["code"].Int(), ErrorCodes::ReauthenticationRequired);
+ ASSERT_EQ(reply["codeName"].String(),
+ ErrorCodes::errorString(ErrorCodes::ReauthenticationRequired));
+ });
+}
+
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/session/logical_session_id_test.cpp b/src/mongo/db/session/logical_session_id_test.cpp
index 736525ecf0f..5b5f59fbb83 100644
--- a/src/mongo/db/session/logical_session_id_test.cpp
+++ b/src/mongo/db/session/logical_session_id_test.cpp
@@ -111,7 +111,7 @@ public:
<< "db"
<< "test"))),
BSONObj()));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un, boost::none));
return authzSession->lookupUser(un);
}
@@ -126,7 +126,7 @@ public:
<< "db"
<< "admin"))),
BSONObj()));
- ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un));
+ ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un, boost::none));
return authzSession->lookupUser(un);
}
};
diff --git a/src/mongo/embedded/embedded_auth_session.cpp b/src/mongo/embedded/embedded_auth_session.cpp
index d086a0a3309..1925afdf14c 100644
--- a/src/mongo/embedded/embedded_auth_session.cpp
+++ b/src/mongo/embedded/embedded_auth_session.cpp
@@ -70,7 +70,9 @@ public:
void startContractTracking() override {}
- Status addAndAuthorizeUser(OperationContext*, const UserName&) override {
+ Status addAndAuthorizeUser(OperationContext*,
+ const UserName&,
+ boost::optional<Date_t>) override {
UASSERT_NOT_IMPLEMENTED;
}
@@ -226,6 +228,10 @@ public:
return true;
}
+ bool isExpired() const override {
+ return false;
+ }
+
protected:
std::tuple<boost::optional<UserName>*, std::vector<RoleName>*> _getImpersonations() override {
UASSERT_NOT_IMPLEMENTED;