Skip to content

Commit 64bf842

Browse files
committed
Adjust SyncUser's internal API to no longer permit invalid states
SyncUser previously allowed setting the state to LoggedIn without setting its tokens, and conversely allowed setting tokens while not logged in. This was error-prone and happened to result in a lock-order inversion due to that both the state and the tokens had to be checked in places where we only wanted to care about one of them.
1 parent a365369 commit 64bf842

File tree

8 files changed

+92
-114
lines changed

8 files changed

+92
-114
lines changed

CHANGELOG.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
- Removing one of the SyncUsers would delete all local Realm files for all SyncUsers for that user.
1212
- Deleting the server-side user via one of the SyncUsers left the other SyncUsers in an invalid state.
1313
- A SyncUser which was originally created via anonymous login and then linked to an identity would still be treated as an anonymous users and removed entirely on logout.
14-
(since v10.0.0)
15-
* Reading existing logged-in users on app startup from the sync metadata Realm performed three no-op writes per user on the metadata Realm.
14+
([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0)
15+
* Reading existing logged-in users on app startup from the sync metadata Realm performed three no-op writes per user on the metadata Realm ([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0).
16+
* If a user was logged out while an access token refresh was in progress, the refresh completing would mark the user as logged in again and the user would be in an inconsistent state ([PR #6837](https://github.com/realm/realm-core/pull/6837), since v10.0.0).
1617

1718
### Breaking changes
18-
* SyncUser::provider_type() and realm_user_get_auth_provider() have been removed. Users don't have provider types; identities do.
19-
* SyncUser no longer has a `local_identity()`. `identity()` has been guaranteed to be unique per App ever since v10.
20-
* SyncUser no longer overrides operator==. Pointer equality should be used to compare sync users.
19+
* SyncUser::provider_type() and realm_user_get_auth_provider() have been removed. Users don't have provider types; identities do. `SyncUser::is_anonymous()` is a more correct version of checking if the provider type is anonymous ([PR #6837](https://github.com/realm/realm-core/pull/6837)).
20+
* SyncUser no longer has a `local_identity()`. `identity()` has been guaranteed to be unique per App ever since v10 ([PR #6837](https://github.com/realm/realm-core/pull/6837)).
21+
* SyncUser no longer overrides operator==. Pointer equality should be used to compare sync users ([PR #6837](https://github.com/realm/realm-core/pull/6837)).
2122

2223
### Compatibility
2324
* Fileformat: Generates files with format v23. Reads and automatically upgrade from fileformat v5.

src/realm/object-store/sync/sync_manager.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ std::shared_ptr<SyncUser> SyncManager::get_user(const std::string& user_id, cons
355355
else { // LoggedOut => LoggedIn
356356
auto user = *it;
357357
REALM_ASSERT(user->state() != SyncUser::State::Removed);
358-
user->update_state_and_tokens(SyncUser::State::LoggedIn, std::move(access_token), std::move(refresh_token));
358+
user->log_in(access_token, refresh_token);
359359
return user;
360360
}
361361
}
@@ -443,10 +443,8 @@ void SyncManager::set_current_user(const std::string& user_id)
443443
void SyncManager::remove_user(const std::string& user_id)
444444
{
445445
util::CheckedLockGuard lock(m_user_mutex);
446-
auto user = get_user_for_identity(user_id);
447-
if (!user)
448-
return;
449-
user->set_state(SyncUser::State::Removed);
446+
if (auto user = get_user_for_identity(user_id))
447+
user->invalidate();
450448
}
451449

452450
void SyncManager::delete_user(const std::string& user_id)

src/realm/object-store/sync/sync_user.cpp

Lines changed: 47 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ SyncUser::SyncUser(const std::string& refresh_token, const std::string& id, cons
9393
, m_device_id(device_id)
9494
, m_sync_manager(sync_manager)
9595
{
96+
REALM_ASSERT(!access_token.empty() && !refresh_token.empty());
9697
{
9798
std::lock_guard lock(s_binding_context_factory_mutex);
9899
if (s_binding_context_factory) {
@@ -120,6 +121,15 @@ SyncUser::SyncUser(const SyncUserMetadata& data, SyncManager* sync_manager)
120121
, m_device_id(data.device_id())
121122
, m_sync_manager(sync_manager)
122123
{
124+
// Check for inconsistent state in the metadata Realm. This shouldn't happen,
125+
// but previous versions could sometimes mark a user as logged in with an
126+
// empty refresh token.
127+
if (m_state == State::LoggedIn && (m_refresh_token.token.empty() || m_access_token.token.empty())) {
128+
m_state = State::LoggedOut;
129+
m_refresh_token = {};
130+
m_access_token = {};
131+
}
132+
123133
{
124134
std::lock_guard lock(s_binding_context_factory_mutex);
125135
if (s_binding_context_factory) {
@@ -132,8 +142,10 @@ std::shared_ptr<SyncManager> SyncUser::sync_manager() const
132142
{
133143
util::CheckedLockGuard lk(m_mutex);
134144
if (m_state == State::Removed) {
135-
throw std::logic_error(util::format(
136-
"Cannot start a sync session for user '%1' because this user has been removed.", identity()));
145+
throw app::AppError(
146+
ErrorCodes::ClientUserNotFound,
147+
util::format("Cannot start a sync session for user '%1' because this user has been removed.",
148+
m_identity));
137149
}
138150
REALM_ASSERT(m_sync_manager);
139151
return m_sync_manager->shared_from_this();
@@ -184,33 +196,22 @@ std::shared_ptr<SyncSession> SyncUser::session_for_on_disk_path(const std::strin
184196
return locked;
185197
}
186198

187-
void SyncUser::update_state_and_tokens(SyncUser::State state, const std::string& access_token,
188-
const std::string& refresh_token)
199+
void SyncUser::log_in(const std::string& access_token, const std::string& refresh_token)
189200
{
201+
REALM_ASSERT(!access_token.empty());
202+
REALM_ASSERT(!refresh_token.empty());
190203
std::vector<std::shared_ptr<SyncSession>> sessions_to_revive;
191204
{
192205
util::CheckedLockGuard lock1(m_mutex);
193206
util::CheckedLockGuard lock2(m_tokens_mutex);
194-
m_state = state;
195-
m_access_token = access_token.empty() ? RealmJWT{} : RealmJWT(access_token);
196-
m_refresh_token = refresh_token.empty() ? RealmJWT{} : RealmJWT(refresh_token);
197-
switch (m_state) {
198-
case State::Removed:
199-
// Call set_state() rather than update_state_and_tokens to remove a user.
200-
REALM_UNREACHABLE();
201-
case State::LoggedIn:
202-
sessions_to_revive = revive_sessions();
203-
break;
204-
case State::LoggedOut: {
205-
REALM_ASSERT(m_access_token == RealmJWT{});
206-
REALM_ASSERT(m_refresh_token == RealmJWT{});
207-
break;
208-
}
209-
}
207+
m_state = State::LoggedIn;
208+
m_access_token = RealmJWT(access_token);
209+
m_refresh_token = RealmJWT(refresh_token);
210+
sessions_to_revive = revive_sessions();
210211

211212
m_sync_manager->perform_metadata_update([&](const auto& manager) {
212213
auto metadata = manager.get_or_make_user_metadata(m_identity);
213-
metadata->set_state_and_tokens(state, access_token, refresh_token);
214+
metadata->set_state_and_tokens(State::LoggedIn, access_token, refresh_token);
214215
});
215216
}
216217
// (Re)activate all pending sessions.
@@ -223,6 +224,23 @@ void SyncUser::update_state_and_tokens(SyncUser::State state, const std::string&
223224
emit_change_to_subscribers(*this);
224225
}
225226

227+
void SyncUser::invalidate()
228+
{
229+
{
230+
util::CheckedLockGuard lock1(m_mutex);
231+
util::CheckedLockGuard lock2(m_tokens_mutex);
232+
m_state = State::Removed;
233+
m_access_token = {};
234+
m_refresh_token = {};
235+
236+
m_sync_manager->perform_metadata_update([&](const auto& manager) {
237+
auto metadata = manager.get_or_make_user_metadata(m_identity);
238+
metadata->set_state_and_tokens(State::Removed, "", "");
239+
});
240+
}
241+
emit_change_to_subscribers(*this);
242+
}
243+
226244
std::vector<std::shared_ptr<SyncSession>> SyncUser::revive_sessions()
227245
{
228246
std::vector<std::shared_ptr<SyncSession>> sessions_to_revive;
@@ -239,37 +257,19 @@ std::vector<std::shared_ptr<SyncSession>> SyncUser::revive_sessions()
239257

240258
void SyncUser::update_access_token(std::string&& token)
241259
{
242-
std::vector<std::shared_ptr<SyncSession>> sessions_to_revive;
243260
{
244261
util::CheckedLockGuard lock(m_mutex);
245-
util::CheckedLockGuard lock2(m_tokens_mutex);
246-
switch (m_state) {
247-
case State::Removed:
248-
return;
249-
case State::LoggedIn:
250-
m_access_token = RealmJWT(std::move(token));
251-
break;
252-
case State::LoggedOut: {
253-
m_access_token = RealmJWT(std::move(token));
254-
m_state = State::LoggedIn;
255-
sessions_to_revive = revive_sessions();
256-
break;
257-
}
258-
}
262+
if (m_state != State::LoggedIn)
263+
return;
259264

265+
util::CheckedLockGuard lock2(m_tokens_mutex);
266+
m_access_token = RealmJWT(std::move(token));
260267
m_sync_manager->perform_metadata_update([&, raw_access_token = m_access_token.token](const auto& manager) {
261268
auto metadata = manager.get_or_make_user_metadata(m_identity);
262269
metadata->set_access_token(raw_access_token);
263270
});
264271
}
265272

266-
// (Re)activate all pending sessions.
267-
// Note that we do this after releasing the lock, since the session may
268-
// need to access protected User state in the process of binding itself.
269-
for (auto& session : sessions_to_revive) {
270-
session->revive_if_needed();
271-
}
272-
273273
emit_change_to_subscribers(*this);
274274
}
275275

@@ -333,13 +333,7 @@ void SyncUser::log_out()
333333
bool SyncUser::is_logged_in() const
334334
{
335335
util::CheckedLockGuard lock(m_mutex);
336-
util::CheckedLockGuard lock2(m_tokens_mutex);
337-
return do_is_logged_in();
338-
}
339-
340-
bool SyncUser::do_is_logged_in() const
341-
{
342-
return !m_access_token.token.empty() && !m_refresh_token.token.empty() && m_state == State::LoggedIn;
336+
return m_state == State::LoggedIn;
343337
}
344338

345339
bool SyncUser::is_anonymous() const
@@ -351,15 +345,10 @@ bool SyncUser::is_anonymous() const
351345

352346
bool SyncUser::do_is_anonymous() const
353347
{
354-
return do_is_logged_in() && m_user_identities.size() == 1 &&
348+
return m_state == State::LoggedIn && m_user_identities.size() == 1 &&
355349
m_user_identities[0].provider_type == app::IdentityProviderAnonymous;
356350
}
357351

358-
void SyncUser::invalidate()
359-
{
360-
set_state(SyncUser::State::Removed);
361-
}
362-
363352
std::string SyncUser::refresh_token() const
364353
{
365354
util::CheckedLockGuard lock(m_tokens_mutex);
@@ -390,18 +379,6 @@ SyncUser::State SyncUser::state() const
390379
return m_state;
391380
}
392381

393-
void SyncUser::set_state(SyncUser::State state)
394-
{
395-
util::CheckedLockGuard lock(m_mutex);
396-
m_state = state;
397-
398-
REALM_ASSERT(m_sync_manager);
399-
m_sync_manager->perform_metadata_update([&](const auto& manager) {
400-
auto metadata = manager.get_or_make_user_metadata(m_identity);
401-
metadata->set_state(state);
402-
});
403-
}
404-
405382
SyncUserProfile SyncUser::user_profile() const
406383
{
407384
util::CheckedLockGuard lock(m_mutex);
@@ -506,12 +483,11 @@ bool SyncUser::access_token_refresh_required() const
506483
{
507484
using namespace std::chrono;
508485
constexpr size_t buffer_seconds = 5; // arbitrary
509-
util::CheckedLockGuard lock(m_mutex);
510-
util::CheckedLockGuard lock2(m_tokens_mutex);
486+
util::CheckedLockGuard lock(m_tokens_mutex);
511487
const auto now = duration_cast<seconds>(system_clock::now().time_since_epoch()).count() +
512488
m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed);
513489
const auto threshold = now - buffer_seconds;
514-
return do_is_logged_in() && m_access_token.expires_at < static_cast<int64_t>(threshold);
490+
return !m_access_token.token.empty() && m_access_token.expires_at < static_cast<int64_t>(threshold);
515491
}
516492

517493
} // namespace realm

src/realm/object-store/sync/sync_user.hpp

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,12 @@ class SyncUser : public std::enable_shared_from_this<SyncUser>, public Subscriba
243243
const std::string& device_id, SyncManager* sync_manager);
244244
SyncUser(const SyncUserMetadata& data, SyncManager* sync_manager);
245245

246-
void set_state(SyncUser::State state) REQUIRES(!m_mutex);
246+
// Atomically set the user to be logged in and update both tokens.
247+
void log_in(const std::string& access_token, const std::string& refresh_token)
248+
REQUIRES(!m_mutex, !m_tokens_mutex);
247249

248-
// Update the user's state and refresh/access tokens atomically in a Realm transaction.
249-
// If the user is transitioning between LoggedIn and LoggedOut, then the access_token and
250-
// refresh token must be empty, and likewise must not be empty if transitioning between
251-
// logged out and logged in.
252-
// Note that this is called by the SyncManager, and should not be directly called.
253-
void update_state_and_tokens(SyncUser::State state, const std::string& access_token,
254-
const std::string& refresh_token) REQUIRES(!m_mutex, !m_tokens_mutex);
250+
// Atomically set the user to be removed and remove tokens.
251+
void invalidate() REQUIRES(!m_mutex, !m_tokens_mutex);
255252

256253
// Update the user's access token. If the user is logged out, it will log itself back in.
257254
// Note that this is called by the SyncManager, and should not be directly called.
@@ -276,7 +273,7 @@ class SyncUser : public std::enable_shared_from_this<SyncUser>, public Subscriba
276273

277274
/// Checks the expiry on the access token against the local time and if it is invalid or expires soon, returns
278275
/// true.
279-
bool access_token_refresh_required() const REQUIRES(!m_mutex, !m_tokens_mutex);
276+
bool access_token_refresh_required() const REQUIRES(!m_tokens_mutex);
280277

281278
// Hook for testing access token timeouts
282279
void set_seconds_to_adjust_time_for_testing(int seconds)
@@ -292,8 +289,7 @@ class SyncUser : public std::enable_shared_from_this<SyncUser>, public Subscriba
292289
static SyncUserContextFactory s_binding_context_factory;
293290
static std::mutex s_binding_context_factory_mutex;
294291

295-
bool do_is_logged_in() const REQUIRES(m_tokens_mutex, m_mutex);
296-
bool do_is_anonymous() const REQUIRES(m_tokens_mutex, m_mutex);
292+
bool do_is_anonymous() const REQUIRES(m_mutex);
297293

298294
std::vector<std::shared_ptr<SyncSession>> revive_sessions() REQUIRES(m_mutex);
299295

@@ -305,9 +301,6 @@ class SyncUser : public std::enable_shared_from_this<SyncUser>, public Subscriba
305301
// used to locate existing files.
306302
std::vector<std::string> m_legacy_identities;
307303

308-
// Mark the user as invalid, since a fatal user-related error was encountered.
309-
void invalidate() REQUIRES(!m_mutex);
310-
311304
mutable util::CheckedMutex m_mutex;
312305

313306
// Set by the server. The unique ID of the user account on the Realm Application.

test/object-store/c_api/c_api.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5006,7 +5006,7 @@ TEST_CASE("C API - async_open", "[sync][pbs][c_api]") {
50065006
realm_sync_config_t* sync_config = realm_sync_config_new(&user, "realm");
50075007
realm_sync_config_set_initial_subscription_handler(sync_config, task_init_subscription, false, nullptr,
50085008
nullptr);
5009-
sync_config->user->update_state_and_tokens(SyncUser::State::LoggedIn, invalid_token, invalid_token);
5009+
sync_config->user->log_in(invalid_token, invalid_token);
50105010

50115011
realm_config_set_path(config, test_config.path.c_str());
50125012
realm_config_set_schema_version(config, 1);

test/object-store/realm.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,7 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") {
11841184
TestSyncManager tsm(tsm_config);
11851185

11861186
SyncTestFile config(tsm.app(), "realm");
1187-
config.sync_config->user->update_state_and_tokens(SyncUser::State::LoggedIn, invalid_token, invalid_token);
1187+
config.sync_config->user->log_in(invalid_token, invalid_token);
11881188

11891189
bool got_error = false;
11901190
config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError) {

test/object-store/sync/app.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,8 +2868,8 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
28682868
std::shared_ptr<SyncUser> user = app->current_user();
28692869
REQUIRE(user);
28702870
REQUIRE(!user->access_token_refresh_required());
2871-
// Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locallaly
2872-
// (access tokens have an lifetime of 30 mintutes today).
2871+
// Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally
2872+
// (access tokens have an lifetime of 30 minutes today).
28732873
user->set_seconds_to_adjust_time_for_testing(31 * 60);
28742874
REQUIRE(user->access_token_refresh_required());
28752875

@@ -2982,6 +2982,17 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
29822982
REQUIRE(!user->is_logged_in());
29832983
}
29842984

2985+
SECTION("User is left logged out if logged out while the refresh is in progress") {
2986+
REQUIRE(user->is_logged_in());
2987+
transport->request_hook = [&](const Request&) {
2988+
user->log_out();
2989+
};
2990+
SyncTestFile config(app, partition, schema);
2991+
auto r = Realm::get_shared_realm(config);
2992+
REQUIRE_FALSE(user->is_logged_in());
2993+
REQUIRE(user->state() == SyncUser::State::LoggedOut);
2994+
}
2995+
29852996
SECTION("Requests that receive an error are retried on a backoff") {
29862997
using namespace std::chrono;
29872998
std::vector<time_point<steady_clock>> response_times;
@@ -3193,11 +3204,10 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
31933204
anon_user->identity()));
31943205
});
31953206

3196-
REQUIRE_THROWS_MATCHES(
3197-
Realm::get_shared_realm(config), std::logic_error,
3198-
Catch::Matchers::Message(
3199-
util::format("Cannot start a sync session for user '%1' because this user has been removed.",
3200-
anon_user->identity())));
3207+
REQUIRE_EXCEPTION(
3208+
Realm::get_shared_realm(config), ClientUserNotFound,
3209+
util::format("Cannot start a sync session for user '%1' because this user has been removed.",
3210+
anon_user->identity()));
32013211
}
32023212

32033213
SECTION("Opening a Realm with a removed email user results produces an exception") {
@@ -3216,11 +3226,11 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
32163226
REQUIRE_FALSE(email_user->is_logged_in());
32173227
REQUIRE(email_user->state() == SyncUser::State::Removed);
32183228

3219-
// should not be able to open a sync'd Realm with an invalid user
3220-
REQUIRE_THROWS_MATCHES(
3221-
Realm::get_shared_realm(config), std::logic_error,
3222-
Catch::Matchers::Message(util::format(
3223-
"Cannot start a sync session for user '%1' because this user has been removed.", user_ident)));
3229+
// should not be able to open a synced Realm with an invalid user
3230+
REQUIRE_EXCEPTION(
3231+
Realm::get_shared_realm(config), ClientUserNotFound,
3232+
util::format("Cannot start a sync session for user '%1' because this user has been removed.",
3233+
user_ident));
32243234

32253235
std::shared_ptr<SyncUser> new_user_instance = log_in(app, creds);
32263236
// the previous instance is still invalid
@@ -4891,7 +4901,7 @@ TEST_CASE("app: app destroyed during token refresh", "[sync][app][user][token]")
48914901
// Ignore these errors, since there's not really an app out there...
48924902
// Primarily make sure we don't crash unexpectedly
48934903
std::vector<const char*> expected_errors = {"Bad WebSocket", "Connection Failed", "user has been removed",
4894-
"Connection refused"};
4904+
"Connection refused", "The user is not logged in"};
48954905
auto expected =
48964906
std::find_if(expected_errors.begin(), expected_errors.end(), [error](const char* err_msg) {
48974907
return error.status.reason().find(err_msg) != std::string::npos;

0 commit comments

Comments
 (0)