diff options
author | Varun Ravichandran <varun.ravichandran@mongodb.com> | 2022-11-11 19:49:15 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-12-16 23:29:04 +0000 |
commit | 946cecc98dfb0047855add41fc344a1ffbb2baa9 (patch) | |
tree | 29ccefaeb61f3066f1344bb6827bb384ab0326c2 | |
parent | ab506f144f44e24addaccbcb755b8d99e7ef29c3 (diff) | |
download | mongo-946cecc98dfb0047855add41fc344a1ffbb2baa9.tar.gz |
SERVER-70701: Allow AuthorizationSession to enforce expiration times
-rw-r--r-- | jstests/auth/impersonation-deny.js | 2 | ||||
-rw-r--r-- | jstests/replsets/auth1.js | 2 | ||||
-rw-r--r-- | src/mongo/base/error_codes.yml | 1 | ||||
-rw-r--r-- | src/mongo/db/auth/access_checks.idl | 2 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session.h | 11 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_impl.cpp | 85 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_impl.h | 16 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_test.cpp | 381 | ||||
-rw-r--r-- | src/mongo/db/auth/sasl_commands.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/auth/sasl_mechanism_registry.h | 9 | ||||
-rw-r--r-- | src/mongo/db/auth/security_token_authentication_guard.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/commands.cpp | 12 | ||||
-rw-r--r-- | src/mongo/db/commands/authentication_commands.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/commands_test.cpp | 188 | ||||
-rw-r--r-- | src/mongo/db/session/logical_session_id_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/embedded/embedded_auth_session.cpp | 8 |
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; |