• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2210

09 Apr 2024 03:41PM UTC coverage: 92.601% (+0.5%) from 92.106%
2210

push

Evergreen

web-flow
Merge pull request #7300 from realm/tg/rework-metadata-storage

Rework sync user handling and metadata storage

102800 of 195548 branches covered (52.57%)

3051 of 3153 new or added lines in 46 files covered. (96.76%)

41 existing lines in 11 files now uncovered.

249129 of 269035 relevant lines covered (92.6%)

46864217.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.69
/src/realm/object-store/sync/impl/app_metadata.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2024 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/sync/impl/app_metadata.hpp>
20

21
#include <realm/object-store/sync/impl/sync_file.hpp>
22
#include <realm/object-store/object_schema.hpp>
23
#include <realm/object-store/object_store.hpp>
24
#include <realm/object-store/property.hpp>
25
#include <realm/object-store/results.hpp>
26
#include <realm/object-store/schema.hpp>
27
#include <realm/object-store/util/scheduler.hpp>
28
#if REALM_PLATFORM_APPLE
29
#include <realm/object-store/impl/apple/keychain_helper.hpp>
30
#endif
31

32
#include <realm/db.hpp>
33
#include <realm/dictionary.hpp>
34
#include <realm/table.hpp>
35

36
using namespace realm;
37
using realm::app::UserData;
38

39
namespace {
40

41
struct CurrentUserSchema {
42
    TableKey table_key;
43
    ColKey user_id;
44

45
    static constexpr const char* table_name = "current_user_identity";
46

47
    void read(Realm& realm)
48
    {
324✔
49
        auto object_schema = realm.schema().find(table_name);
324✔
50
        table_key = object_schema->table_key;
324✔
51
        user_id = object_schema->persisted_properties[0].column_key;
324✔
52
    }
324✔
53

54
    static ObjectSchema object_schema()
55
    {
324✔
56
        return {table_name, {{table_name, PropertyType::String}}};
324✔
57
    }
324✔
58
};
59

60
struct UserIdentitySchema {
61
    TableKey table_key;
62
    ColKey user_id;
63
    ColKey provider_id;
64

65
    static constexpr const char* table_name = "UserIdentity";
66

67
    void read(Realm& realm)
68
    {
324✔
69
        auto object_schema = realm.schema().find(table_name);
324✔
70
        table_key = object_schema->table_key;
324✔
71
        user_id = object_schema->persisted_properties[0].column_key;
324✔
72
        provider_id = object_schema->persisted_properties[1].column_key;
324✔
73
    }
324✔
74

75
    static ObjectSchema object_schema()
76
    {
324✔
77
        return {table_name,
324✔
78
                ObjectSchema::ObjectType::Embedded,
324✔
79
                {
324✔
80
                    {"id", PropertyType::String},
324✔
81
                    {"provider_type", PropertyType::String},
324✔
82
                }};
324✔
83
    }
324✔
84
};
85

86
struct SyncUserSchema {
87
    TableKey table_key;
88

89
    // The server-supplied user_id for the user. Unique per server instance.
90
    ColKey user_id_col;
91
    // Locally generated UUIDs for the user. These are tracked to be able
92
    // to open pre-existing Realm files, but are no longer generated or
93
    // used for anything else.
94
    ColKey legacy_uuids_col;
95
    // The cached refresh token for this user.
96
    ColKey refresh_token_col;
97
    // The cached access token for this user.
98
    ColKey access_token_col;
99
    // The identities for this user.
100
    ColKey identities_col;
101
    // The current state of this user.
102
    ColKey state_col;
103
    // The device id of this user.
104
    ColKey device_id_col;
105
    // Any additional profile attributes, formatted as a bson string.
106
    ColKey profile_dump_col;
107
    // The set of absolute file paths to Realms belonging to this user.
108
    ColKey realm_file_paths_col;
109

110
    static constexpr const char* table_name = "UserMetadata";
111

112
    void read(Realm& realm)
113
    {
327✔
114
        auto object_schema = realm.schema().find(table_name);
327✔
115
        table_key = object_schema->table_key;
327✔
116
        user_id_col = object_schema->persisted_properties[0].column_key;
327✔
117
        legacy_uuids_col = object_schema->persisted_properties[1].column_key;
327✔
118
        refresh_token_col = object_schema->persisted_properties[2].column_key;
327✔
119
        access_token_col = object_schema->persisted_properties[3].column_key;
327✔
120
        identities_col = object_schema->persisted_properties[4].column_key;
327✔
121
        state_col = object_schema->persisted_properties[5].column_key;
327✔
122
        device_id_col = object_schema->persisted_properties[6].column_key;
327✔
123
        profile_dump_col = object_schema->persisted_properties[7].column_key;
327✔
124
        realm_file_paths_col = object_schema->persisted_properties[8].column_key;
327✔
125
    }
327✔
126

127
    static ObjectSchema object_schema()
128
    {
324✔
129
        return {table_name,
324✔
130
                {{"identity", PropertyType::String},
324✔
131
                 {"legacy_uuids", PropertyType::String | PropertyType::Array},
324✔
132
                 {"refresh_token", PropertyType::String | PropertyType::Nullable},
324✔
133
                 {"access_token", PropertyType::String | PropertyType::Nullable},
324✔
134
                 {"identities", PropertyType::Object | PropertyType::Array, UserIdentitySchema::table_name},
324✔
135
                 {"state", PropertyType::Int},
324✔
136
                 {"device_id", PropertyType::String},
324✔
137
                 {"profile_data", PropertyType::String},
324✔
138
                 {"local_realm_paths", PropertyType::Set | PropertyType::String}}};
324✔
139
    }
324✔
140
};
141

142
struct FileActionSchema {
143
    TableKey table_key;
144

145
    // The original path on disk of the file (generally, the main file for an on-disk Realm).
146
    ColKey idx_original_name;
147
    // A new path on disk for a file to be written to. Context-dependent.
148
    ColKey idx_new_name;
149
    // An enum describing the action to take.
150
    ColKey idx_action;
151
    // The partition key of the Realm.
152
    ColKey idx_partition;
153
    // The user_id of the user to whom the file action applies (despite the internal column name).
154
    ColKey idx_user_identity;
155

156
    static constexpr const char* table_name = "FileActionMetadata";
157

158
    void read(Realm& realm)
159
    {
324✔
160
        auto object_schema = realm.schema().find(table_name);
324✔
161
        table_key = object_schema->table_key;
324✔
162
        idx_original_name = object_schema->persisted_properties[0].column_key;
324✔
163
        idx_new_name = object_schema->persisted_properties[1].column_key;
324✔
164
        idx_action = object_schema->persisted_properties[2].column_key;
324✔
165
        idx_partition = object_schema->persisted_properties[3].column_key;
324✔
166
        idx_user_identity = object_schema->persisted_properties[4].column_key;
324✔
167
    }
324✔
168

169
    static ObjectSchema object_schema()
170
    {
324✔
171
        return {table_name,
324✔
172
                {
324✔
173
                    {"original_name", PropertyType::String, Property::IsPrimary{true}},
324✔
174
                    {"new_name", PropertyType::String | PropertyType::Nullable},
324✔
175
                    {"action", PropertyType::Int},
324✔
176
                    {"url", PropertyType::String},      // actually partition key
324✔
177
                    {"identity", PropertyType::String}, // actually user id
324✔
178
                }};
324✔
179
    }
324✔
180
};
181

182
void migrate_to_v7(std::shared_ptr<Realm> old_realm, std::shared_ptr<Realm> realm)
183
{
3✔
184
    // Before schema version 7 there may have been multiple UserMetadata entries
185
    // for a single user_id with different provider types, so we need to merge
186
    // any duplicates together
187

188
    SyncUserSchema schema;
3✔
189
    schema.read(*realm);
3✔
190

191
    TableRef table = realm->read_group().get_table(schema.table_key);
3✔
192
    TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), SyncUserSchema::table_name);
3✔
193
    if (table->is_empty())
3✔
NEW
194
        return;
×
195
    REALM_ASSERT(table->size() == old_table->size());
3✔
196

197
    ColKey old_uuid_col = old_table->get_column_key("local_uuid");
3✔
198

199
    std::unordered_map<std::string, Obj> users;
3✔
200
    for (size_t i = 0, j = 0; i < table->size(); ++j) {
18✔
201
        auto obj = table->get_object(i);
15✔
202

203
        // Move the local uuid from the old column to the list
204
        auto old_obj = old_table->get_object(j);
15✔
205
        obj.get_list<String>(schema.legacy_uuids_col).add(old_obj.get<String>(old_uuid_col));
15✔
206

207
        // Check if we've already seen an object with the same id. If not, store
208
        // this one and move on
209
        std::string user_id = obj.get<String>(schema.user_id_col);
15✔
210
        auto& existing = users[obj.get<String>(schema.user_id_col)];
15✔
211
        if (!existing.is_valid()) {
15✔
212
            existing = obj;
9✔
213
            ++i;
9✔
214
            continue;
9✔
215
        }
9✔
216

217
        // We have a second object for the same id, so we need to merge them.
218
        // First we merge the state: if one is logged in and the other isn't,
219
        // we'll use the logged-in state and tokens. If both are logged in, we'll
220
        // use the more recent login. If one is logged out and the other is
221
        // removed we'll use the logged out state. If both are logged out or
222
        // both are removed then it doesn't matter which we pick.
223
        using State = SyncUser::State;
6✔
224
        auto state = State(obj.get<int64_t>(schema.state_col));
6✔
225
        auto existing_state = State(existing.get<int64_t>(schema.state_col));
6✔
226
        if (state == existing_state) {
6✔
227
            if (state == State::LoggedIn) {
2✔
228
                RealmJWT token_1(existing.get<StringData>(schema.access_token_col));
2✔
229
                RealmJWT token_2(obj.get<StringData>(schema.access_token_col));
2✔
230
                if (token_1.issued_at < token_2.issued_at) {
2✔
231
                    existing.set(schema.refresh_token_col, obj.get<StringData>(schema.refresh_token_col));
1✔
232
                    existing.set(schema.access_token_col, obj.get<StringData>(schema.access_token_col));
1✔
233
                }
1✔
234
            }
2✔
235
        }
2✔
236
        else if (state == State::LoggedIn || existing_state == State::Removed) {
4✔
237
            existing.set(schema.state_col, int64_t(state));
2✔
238
            existing.set(schema.refresh_token_col, obj.get<StringData>(schema.refresh_token_col));
2✔
239
            existing.set(schema.access_token_col, obj.get<StringData>(schema.access_token_col));
2✔
240
        }
2✔
241

242
        // Next we merge the list properties (identities, legacy uuids, realm file paths)
243
        {
6✔
244
            auto dest = existing.get_linklist(schema.identities_col);
6✔
245
            auto src = obj.get_linklist(schema.identities_col);
6✔
246
            for (size_t i = 0, size = src.size(); i < size; ++i) {
18✔
247
                if (dest.find_first(src.get(i)) == npos) {
12✔
248
                    dest.add(src.get(i));
6✔
249
                }
6✔
250
            }
12✔
251
        }
6✔
252
        {
6✔
253
            auto dest = existing.get_list<String>(schema.legacy_uuids_col);
6✔
254
            auto src = obj.get_list<String>(schema.legacy_uuids_col);
6✔
255
            for (size_t i = 0, size = src.size(); i < size; ++i) {
12✔
256
                if (dest.find_first(src.get(i)) == npos) {
6✔
257
                    dest.add(src.get(i));
6✔
258
                }
6✔
259
            }
6✔
260
        }
6✔
261
        {
6✔
262
            auto dest = existing.get_set<String>(schema.realm_file_paths_col);
6✔
263
            auto src = obj.get_set<String>(schema.realm_file_paths_col);
6✔
264
            for (size_t i = 0, size = src.size(); i < size; ++i) {
18✔
265
                dest.insert(src.get(i));
12✔
266
            }
12✔
267
        }
6✔
268

269
        // Finally we delete the duplicate object. We don't increment `i` as it's
270
        // now the index of the object just after the one we're deleting.
271
        obj.remove();
6✔
272
    }
6✔
273
}
3✔
274

275
std::shared_ptr<Realm> try_get_realm(const RealmConfig& config)
NEW
276
{
×
NEW
277
    try {
×
NEW
278
        return Realm::get_shared_realm(config);
×
NEW
279
    }
×
NEW
280
    catch (const InvalidDatabase&) {
×
NEW
281
        return nullptr;
×
NEW
282
    }
×
NEW
283
}
×
284

285
std::shared_ptr<Realm> open_realm(RealmConfig& config, const app::AppConfig& app_config)
286
{
324✔
287
    bool should_encrypt = app_config.metadata_mode == app::AppConfig::MetadataMode::Encryption;
324✔
288
    if (!REALM_PLATFORM_APPLE && should_encrypt && !app_config.custom_encryption_key)
324!
NEW
289
        throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided.");
×
290

291
    if (app_config.custom_encryption_key && should_encrypt)
324✔
292
        config.encryption_key = *app_config.custom_encryption_key;
5✔
293
    if (app_config.custom_encryption_key || !should_encrypt || !REALM_PLATFORM_APPLE) {
324✔
294
        config.clear_on_invalid_file = true;
324✔
295
        return Realm::get_shared_realm(config);
324✔
296
    }
324✔
297

NEW
298
#if REALM_PLATFORM_APPLE
×
299
    // This logic is all a giant race condition once we have multi-process sync.
300
    // Wrapping it all (including the keychain accesses) in DB::call_with_lock()
301
    // might suffice.
302

303
    // First try to open the Realm with a key already stored in the keychain.
304
    // This works for both the case where everything is sensible and valid and
305
    // when we have a key but no metadata Realm.
NEW
306
    auto key = keychain::get_existing_metadata_realm_key(app_config.app_id, app_config.security_access_group);
×
NEW
307
    if (key) {
×
NEW
308
        config.encryption_key = *key;
×
NEW
309
        if (auto realm = try_get_realm(config))
×
NEW
310
            return realm;
×
NEW
311
    }
×
312

313
    // If we have an existing file and either no key or the key didn't work to
314
    // decrypt it, then we might have an unencrypted metadata Realm resulting
315
    // from a previous run being unable to access the keychain.
NEW
316
    if (util::File::exists(config.path)) {
×
NEW
317
        config.encryption_key.clear();
×
NEW
318
        if (auto realm = try_get_realm(config))
×
NEW
319
            return realm;
×
320

321
        // We weren't able to open the existing file with either the stored key
322
        // or no key, so just recreate it
NEW
323
        config.clear_on_invalid_file = true;
×
NEW
324
    }
×
325

326
    // We now have no metadata Realm. If we don't have an existing stored key,
327
    // try to create and store a new one. This might fail, in which case we
328
    // just create an unencrypted Realm file.
NEW
329
    if (!key)
×
NEW
330
        key = keychain::create_new_metadata_realm_key(app_config.app_id, app_config.security_access_group);
×
NEW
331
    if (key)
×
NEW
332
        config.encryption_key = std::move(*key);
×
NEW
333
    return try_get_realm(config);
×
334
#else  // REALM_PLATFORM_APPLE
335
    REALM_UNREACHABLE();
336
#endif // REALM_PLATFORM_APPLE
NEW
337
}
×
338

339
struct PersistedSyncMetadataManager : public app::MetadataStore {
340
    RealmConfig m_config;
341
    SyncUserSchema m_user_schema;
342
    FileActionSchema m_file_action_schema;
343
    UserIdentitySchema m_user_identity_schema;
344
    CurrentUserSchema m_current_user_schema;
345

346
    PersistedSyncMetadataManager(std::string path, const app::AppConfig& app_config, SyncFileManager& file_manager)
347
    {
324✔
348
        // Note that there are several deferred schema changes which don't
349
        // justify bumping the schema version by themself, but should be done
350
        // the next time something does justify a migration.
351
        // These include:
352
        // - remove FileActionSchema url and identity columns
353
        // - rename current_user_identity to CurrentUserId
354
        // - change most of the nullable columns to non-nullable
355
        constexpr uint64_t SCHEMA_VERSION = 7;
324✔
356

357
        m_config.automatic_change_notifications = false;
324✔
358
        m_config.path = std::move(path);
324✔
359
        m_config.schema = Schema{
324✔
360
            UserIdentitySchema::object_schema(),
324✔
361
            SyncUserSchema::object_schema(),
324✔
362
            FileActionSchema::object_schema(),
324✔
363
            CurrentUserSchema::object_schema(),
324✔
364
        };
324✔
365

366
        m_config.schema_version = SCHEMA_VERSION;
324✔
367
        m_config.schema_mode = SchemaMode::Automatic;
324✔
368
        m_config.scheduler = util::Scheduler::make_dummy();
324✔
369
        m_config.automatically_handle_backlinks_in_migrations = true;
324✔
370
        m_config.migration_function = [](std::shared_ptr<Realm> old_realm, std::shared_ptr<Realm> realm, Schema&) {
3✔
371
            if (old_realm->schema_version() < 7) {
3✔
372
                migrate_to_v7(old_realm, realm);
3✔
373
            }
3✔
374
        };
3✔
375

376
        auto realm = open_realm(m_config, app_config);
324✔
377
        m_user_schema.read(*realm);
324✔
378
        m_file_action_schema.read(*realm);
324✔
379
        m_user_identity_schema.read(*realm);
324✔
380
        m_current_user_schema.read(*realm);
324✔
381

382
        realm->begin_transaction();
324✔
383
        perform_file_actions(*realm, file_manager);
324✔
384
        remove_dead_users(*realm, file_manager);
324✔
385
        realm->commit_transaction();
324✔
386
    }
324✔
387

388
    std::shared_ptr<Realm> get_realm() const
389
    {
3,874✔
390
        return Realm::get_shared_realm(m_config);
3,874✔
391
    }
3,874✔
392

393
    void remove_dead_users(Realm& realm, SyncFileManager& file_manager)
394
    {
324✔
395
        auto& schema = m_user_schema;
324✔
396
        TableRef table = realm.read_group().get_table(schema.table_key);
324✔
397
        for (auto obj : *table) {
25✔
398
            if (static_cast<SyncUser::State>(obj.get<int64_t>(schema.state_col)) == SyncUser::State::Removed) {
25✔
399
                delete_user_realms(file_manager, obj);
7✔
400
            }
7✔
401
        }
25✔
402
    }
324✔
403

404
    void delete_user_realms(SyncFileManager& file_manager, Obj& obj)
405
    {
16✔
406
        Set<StringData> paths = obj.get_set<StringData>(m_user_schema.realm_file_paths_col);
16✔
407
        bool any_failed = false;
16✔
408
        for (auto path : paths) {
7✔
409
            if (!file_manager.remove_realm(path))
7✔
410
                any_failed = true;
1✔
411
        }
7✔
412
        try {
16✔
413
            file_manager.remove_user_realms(obj.get<String>(m_user_schema.user_id_col));
16✔
414
        }
16✔
NEW
415
        catch (FileAccessError const&) {
×
NEW
416
            any_failed = true;
×
NEW
417
        }
×
418

419
        // Only remove the object if all of the tracked realms no longer exist,
420
        // and otherwise try again to delete them on the next launch
421
        if (!any_failed) {
16✔
422
            obj.remove();
15✔
423
        }
15✔
424
    }
16✔
425

426
    bool perform_file_action(SyncFileManager& file_manager, Obj& obj)
427
    {
12✔
428
        auto& schema = m_file_action_schema;
12✔
429
        switch (static_cast<SyncFileAction>(obj.get<int64_t>(schema.idx_action))) {
12✔
430
            case SyncFileAction::DeleteRealm:
6✔
431
                // Delete all the files for the given Realm.
432
                return file_manager.remove_realm(obj.get<String>(schema.idx_original_name));
6✔
433

434
            case SyncFileAction::BackUpThenDeleteRealm:
6✔
435
                // Copy the primary Realm file to the recovery dir, and then delete the Realm.
436
                auto new_name = obj.get<String>(schema.idx_new_name);
6✔
437
                auto original_name = obj.get<String>(schema.idx_original_name);
6✔
438
                if (!util::File::exists(original_name)) {
6✔
439
                    // The Realm file doesn't exist anymore, which is fine
440
                    return true;
1✔
441
                }
1✔
442

443
                if (new_name && file_manager.copy_realm_file(original_name, new_name)) {
5✔
444
                    // We successfully copied the Realm file to the recovery directory.
445
                    bool did_remove = file_manager.remove_realm(original_name);
4✔
446
                    // if the copy succeeded but not the delete, then running BackupThenDelete
447
                    // a second time would fail, so change this action to just delete the original file.
448
                    if (did_remove) {
4✔
449
                        return true;
3✔
450
                    }
3✔
451
                    obj.set(schema.idx_action, static_cast<int64_t>(SyncFileAction::DeleteRealm));
1✔
452
                }
1✔
453
        }
12✔
454
        return false;
2✔
455
    }
12✔
456

457
    void perform_file_actions(Realm& realm, SyncFileManager& file_manager)
458
    {
324✔
459
        TableRef table = realm.read_group().get_table(m_file_action_schema.table_key);
324✔
460
        if (table->is_empty())
324✔
461
            return;
323✔
462

463
        for (auto obj : *table) {
2✔
464
            if (perform_file_action(file_manager, obj))
2✔
465
                obj.remove();
2✔
466
        }
2✔
467
    }
1✔
468

469
    bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view realm_path) override
470
    {
21✔
471
        auto realm = get_realm();
21✔
472
        realm->begin_transaction();
21✔
473
        TableRef table = realm->read_group().get_table(m_file_action_schema.table_key);
21✔
474
        auto key = table->where().equal(m_file_action_schema.idx_original_name, StringData(realm_path)).find();
21✔
475
        if (!key) {
21✔
476
            return false;
11✔
477
        }
11✔
478
        auto obj = table->get_object(key);
10✔
479
        bool did_run = perform_file_action(file_manager, obj);
10✔
480
        if (did_run)
10✔
481
            obj.remove();
8✔
482
        realm->commit_transaction();
10✔
483
        return did_run;
10✔
484
    }
10✔
485

486
    bool has_logged_in_user(std::string_view user_id) override
487
    {
36✔
488
        auto realm = get_realm();
36✔
489
        auto obj = find_user(*realm, user_id);
36✔
490
        return is_valid_user(obj);
36✔
491
    }
36✔
492

493
    std::optional<UserData> get_user(std::string_view user_id) override
494
    {
1,552✔
495
        auto realm = get_realm();
1,552✔
496
        return read_user(find_user(*realm, user_id));
1,552✔
497
    }
1,552✔
498

499
    void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token,
500
                     std::string_view device_id) override
501
    {
568✔
502
        auto realm = get_realm();
568✔
503
        realm->begin_transaction();
568✔
504

505
        auto& schema = m_user_schema;
568✔
506
        Obj obj = find_user(*realm, user_id);
568✔
507
        if (!obj) {
568✔
508
            obj = realm->read_group().get_table(m_user_schema.table_key)->create_object();
553✔
509
            obj.set<String>(schema.user_id_col, user_id);
553✔
510

511
            // Mark the user we just created as the current user
512
            Obj current_user = current_user_obj(*realm);
553✔
513
            current_user.set<String>(m_current_user_schema.user_id, user_id);
553✔
514
        }
553✔
515

516
        obj.set(schema.state_col, (int64_t)SyncUser::State::LoggedIn);
568✔
517
        obj.set<String>(schema.refresh_token_col, refresh_token);
568✔
518
        obj.set<String>(schema.access_token_col, access_token);
568✔
519
        obj.set<String>(schema.device_id_col, device_id);
568✔
520

521
        realm->commit_transaction();
568✔
522
    }
568✔
523

524
    void update_user(std::string_view user_id, const UserData& data) override
525
    {
554✔
526
        auto realm = get_realm();
554✔
527
        realm->begin_transaction();
554✔
528
        auto& schema = m_user_schema;
554✔
529
        Obj obj = find_user(*realm, user_id);
554✔
530
        REALM_ASSERT(obj);
554✔
531
        obj.set(schema.state_col,
554✔
532
                int64_t(data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut));
552✔
533
        obj.set<String>(schema.refresh_token_col, data.refresh_token.token);
554✔
534
        obj.set<String>(schema.access_token_col, data.access_token.token);
554✔
535
        obj.set<String>(schema.device_id_col, data.device_id);
554✔
536

537
        std::stringstream profile;
554✔
538
        profile << data.profile.data();
554✔
539
        obj.set(schema.profile_dump_col, profile.str());
554✔
540

541
        auto identities_list = obj.get_linklist(schema.identities_col);
554✔
542
        identities_list.clear();
554✔
543

544
        for (auto& ident : data.identities) {
556✔
545
            auto obj = identities_list.create_and_insert_linked_object(identities_list.size());
556✔
546
            obj.set<String>(m_user_identity_schema.user_id, ident.id);
556✔
547
            obj.set<String>(m_user_identity_schema.provider_id, ident.provider_type);
556✔
548
        }
556✔
549

550
        // intentionally does not update `legacy_identities` as that field is
551
        // read-only and no longer used
552

553
        realm->commit_transaction();
554✔
554
    }
554✔
555

556
    Obj current_user_obj(Realm& realm) const
557
    {
1,620✔
558
        TableRef current_user_table = realm.read_group().get_table(m_current_user_schema.table_key);
1,620✔
559
        Obj obj;
1,620✔
560
        if (!current_user_table->is_empty())
1,620✔
561
            obj = *current_user_table->begin();
1,322✔
562
        else if (realm.is_in_transaction())
298✔
563
            obj = current_user_table->create_object();
296✔
564
        return obj;
1,620✔
565
    }
1,620✔
566

567
    // Some of our string columns are nullable. They never should actually be
568
    // null as we store "" rather than null when the value isn't present, but
569
    // be safe and handle it anyway.
570
    static std::string get_string(const Obj& obj, ColKey col)
571
    {
5,587✔
572
        auto str = obj.get<String>(col);
5,587✔
573
        return str.is_null() ? "" : str;
5,587✔
574
    }
5,587✔
575

576
    std::optional<app::UserData> read_user(const Obj& obj) const
577
    {
1,552✔
578
        if (!obj) {
1,552✔
NEW
579
            return {};
×
NEW
580
        }
×
581
        auto state = SyncUser::State(obj.get<Int>(m_user_schema.state_col));
1,552✔
582
        if (state == SyncUser::State::Removed) {
1,552✔
583
            return {};
9✔
584
        }
9✔
585

586
        UserData data;
1,543✔
587
        if (state == SyncUser::State::LoggedIn) {
1,543✔
588
            try {
1,533✔
589
                data.access_token = RealmJWT(get_string(obj, m_user_schema.access_token_col));
1,533✔
590
                data.refresh_token = RealmJWT(get_string(obj, m_user_schema.refresh_token_col));
1,533✔
591
            }
1,533✔
592
            catch (...) {
1✔
593
                // Invalid stored token results in a logged-out user
594
                data.access_token = {};
1✔
595
                data.refresh_token = {};
1✔
596
            }
1✔
597
        }
1,533✔
598

599
        data.device_id = get_string(obj, m_user_schema.device_id_col);
1,543✔
600
        if (auto profile = obj.get<String>(m_user_schema.profile_dump_col); profile.size()) {
1,543✔
601
            data.profile = static_cast<bson::BsonDocument>(bson::parse(std::string_view(profile)));
482✔
602
        }
482✔
603

604
        auto identities_list = obj.get_linklist(m_user_schema.identities_col);
1,543✔
605
        auto identities_table = identities_list.get_target_table();
1,543✔
606
        data.identities.reserve(identities_list.size());
1,543✔
607
        for (size_t i = 0, size = identities_list.size(); i < size; ++i) {
2,045✔
608
            auto obj = identities_table->get_object(identities_list.get(i));
502✔
609
            data.identities.push_back({obj.get<String>(m_user_identity_schema.user_id),
502✔
610
                                       obj.get<String>(m_user_identity_schema.provider_id)});
502✔
611
        }
502✔
612

613
        auto legacy_identities = obj.get_list<String>(m_user_schema.legacy_uuids_col);
1,543✔
614
        data.legacy_identities.reserve(legacy_identities.size());
1,543✔
615
        for (size_t i = 0, size = legacy_identities.size(); i < size; ++i) {
1,559✔
616
            data.legacy_identities.push_back(legacy_identities.get(i));
16✔
617
        }
16✔
618

619
        return data;
1,543✔
620
    }
1,543✔
621

622
    void update_current_user(Realm& realm, std::string_view removed_user_id)
623
    {
38✔
624
        auto current_user = current_user_obj(realm);
38✔
625
        if (current_user.get<String>(m_current_user_schema.user_id) == removed_user_id) {
38✔
626
            // Set to either empty or the first still logged in user
627
            current_user.set(m_current_user_schema.user_id, get_current_user());
33✔
628
        }
33✔
629
    }
38✔
630

631
    void log_out(std::string_view user_id, SyncUser::State new_state) override
632
    {
29✔
633
        REALM_ASSERT(new_state != SyncUser::State::LoggedIn);
29✔
634
        auto realm = get_realm();
29✔
635
        realm->begin_transaction();
29✔
636
        if (auto obj = find_user(*realm, user_id)) {
29✔
637
            obj.set(m_user_schema.state_col, (int64_t)new_state);
29✔
638
            obj.set<String>(m_user_schema.access_token_col, "");
29✔
639
            obj.set<String>(m_user_schema.refresh_token_col, "");
29✔
640
            update_current_user(*realm, user_id);
29✔
641
        }
29✔
642
        realm->commit_transaction();
29✔
643
    }
29✔
644

645
    void delete_user(SyncFileManager& file_manager, std::string_view user_id) override
646
    {
10✔
647
        auto realm = get_realm();
10✔
648
        realm->begin_transaction();
10✔
649
        if (auto obj = find_user(*realm, user_id)) {
10✔
650
            delete_user_realms(file_manager, obj); // also removes obj
9✔
651
            update_current_user(*realm, user_id);
9✔
652
        }
9✔
653
        realm->commit_transaction();
10✔
654
    }
10✔
655

656
    void add_realm_path(std::string_view user_id, std::string_view path) override
657
    {
8✔
658
        auto realm = get_realm();
8✔
659
        realm->begin_transaction();
8✔
660
        if (auto obj = find_user(*realm, user_id)) {
8✔
661
            obj.get_set<String>(m_user_schema.realm_file_paths_col).insert(path);
8✔
662
        }
8✔
663
        realm->commit_transaction();
8✔
664
    }
8✔
665

666
    bool is_valid_user(Obj& obj)
667
    {
543✔
668
        // This is overly cautious and merely checking the state should suffice,
669
        // but because this is a persisted file that can be modified it's possible
670
        // to get invalid combinations of data.
671
        return obj && obj.get<int64_t>(m_user_schema.state_col) == int64_t(SyncUser::State::LoggedIn) &&
543✔
672
               RealmJWT::validate(get_string(obj, m_user_schema.access_token_col)) &&
491✔
673
               RealmJWT::validate(get_string(obj, m_user_schema.refresh_token_col));
488✔
674
    }
543✔
675

676
    std::vector<std::string> get_all_users() override
677
    {
23✔
678
        auto realm = get_realm();
23✔
679
        auto table = realm->read_group().get_table(m_user_schema.table_key);
23✔
680
        std::vector<std::string> users;
23✔
681
        users.reserve(table->size());
23✔
682
        for (auto& obj : *table) {
31✔
683
            if (obj.get<int64_t>(m_user_schema.state_col) != int64_t(SyncUser::State::Removed)) {
31✔
684
                users.emplace_back(obj.get<String>(m_user_schema.user_id_col));
22✔
685
            }
22✔
686
        }
31✔
687
        return users;
23✔
688
    }
23✔
689

690
    std::string get_current_user() override
691
    {
490✔
692
        auto realm = get_realm();
490✔
693
        if (auto obj = current_user_obj(*realm)) {
490✔
694
            auto user_id = obj.get<String>(m_current_user_schema.user_id);
488✔
695
            auto user_obj = find_user(*realm, user_id);
488✔
696
            if (is_valid_user(user_obj)) {
488✔
697
                return user_id;
472✔
698
            }
472✔
699
        }
18✔
700

701
        auto table = realm->read_group().get_table(m_user_schema.table_key);
18✔
702
        for (auto& obj : *table) {
19✔
703
            if (is_valid_user(obj)) {
19✔
704
                return obj.get<String>(m_user_schema.user_id_col);
5✔
705
            }
5✔
706
        }
19✔
707

708
        return "";
13✔
709
    }
18✔
710

711
    void set_current_user(std::string_view user_id) override
712
    {
539✔
713
        auto realm = get_realm();
539✔
714
        realm->begin_transaction();
539✔
715
        current_user_obj(*realm).set<String>(m_current_user_schema.user_id, user_id);
539✔
716
        realm->commit_transaction();
539✔
717
    }
539✔
718

719
    void create_file_action(SyncFileAction action, std::string_view original_path,
720
                            std::string_view recovery_path) override
721
    {
44✔
722
        REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !recovery_path.empty());
44✔
723

724
        auto realm = get_realm();
44✔
725
        realm->begin_transaction();
44✔
726
        TableRef table = realm->read_group().get_table(m_file_action_schema.table_key);
44✔
727
        Obj obj = table->create_object_with_primary_key(original_path);
44✔
728
        obj.set(m_file_action_schema.idx_new_name, recovery_path);
44✔
729
        obj.set(m_file_action_schema.idx_action, static_cast<int64_t>(action));
44✔
730
        // There's also partition and user_id fields in the schema, but they
731
        // aren't actually used for anything and are never read
732
        realm->commit_transaction();
44✔
733
    }
44✔
734

735
    Obj find_user(Realm& realm, StringData user_id) const
736
    {
3,245✔
737
        Obj obj;
3,245✔
738
        if (user_id.size() == 0)
3,245✔
739
            return obj;
4✔
740

741
        auto table = realm.read_group().get_table(m_user_schema.table_key);
3,241✔
742
        Query q = table->where().equal(m_user_schema.user_id_col, user_id);
3,241✔
743
        REALM_ASSERT_DEBUG(q.count() < 2); // user_id_col ought to be a primary key
3,241✔
744
        if (auto key = q.find())
3,241✔
745
            obj = table->get_object(key);
2,667✔
746
        return obj;
3,241✔
747
    }
3,241✔
748
};
749

750
class InMemoryMetadataStorage : public app::MetadataStore {
751
    std::mutex m_mutex;
752
    std::map<std::string, UserData, std::less<>> m_users;
753
    std::map<std::string, std::set<std::string>, std::less<>> m_realm_paths;
754
    std::string m_active_user;
755
    struct FileAction {
756
        SyncFileAction action;
757
        std::string backup_path;
758
    };
759
    std::map<std::string, FileAction, std::less<>> m_file_actions;
760

761
    bool has_logged_in_user(std::string_view user_id) override
762
    {
30✔
763
        std::lock_guard lock(m_mutex);
30✔
764
        auto it = m_users.find(user_id);
30✔
765
        return it != m_users.end() && it->second.access_token;
30✔
766
    }
30✔
767

768
    std::optional<UserData> get_user(std::string_view user_id) override
769
    {
5,529✔
770
        std::lock_guard lock(m_mutex);
5,529✔
771
        if (auto it = m_users.find(user_id); it != m_users.end()) {
5,529✔
772
            return it->second;
5,521✔
773
        }
5,521✔
774
        return {};
8✔
775
    }
8✔
776

777
    void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token,
778
                     std::string_view device_id) override
779
    {
1,872✔
780
        std::lock_guard lock(m_mutex);
1,872✔
781
        auto it = m_users.find(user_id);
1,872✔
782
        if (it == m_users.end()) {
1,872✔
783
            it = m_users.insert({std::string(user_id), UserData{}}).first;
1,863✔
784
            m_active_user = user_id;
1,863✔
785
        }
1,863✔
786
        auto& user = it->second;
1,872✔
787
        user.device_id = device_id;
1,872✔
788
        try {
1,872✔
789
            user.refresh_token = RealmJWT(refresh_token);
1,872✔
790
            user.access_token = RealmJWT(access_token);
1,872✔
791
        }
1,872✔
792
        catch (...) {
8✔
793
            user.refresh_token = {};
8✔
794
            user.access_token = {};
8✔
795
        }
8✔
796
    }
1,872✔
797

798
    void update_user(std::string_view user_id, const UserData& data) override
799
    {
1,856✔
800
        std::lock_guard lock(m_mutex);
1,856✔
801
        auto& user = m_users.find(user_id)->second;
1,856✔
802
        user = data;
1,856✔
803
        user.legacy_identities.clear();
1,856✔
804
    }
1,856✔
805

806
    void log_out(std::string_view user_id, SyncUser::State new_state) override
807
    {
17✔
808
        std::lock_guard lock(m_mutex);
17✔
809
        if (auto it = m_users.find(user_id); it != m_users.end()) {
17✔
810
            if (new_state == SyncUser::State::Removed) {
17✔
811
                m_users.erase(it);
11✔
812
            }
11✔
813
            else {
6✔
814
                auto& user = it->second;
6✔
815
                user.access_token = {};
6✔
816
                user.refresh_token = {};
6✔
817
                user.device_id.clear();
6✔
818
            }
6✔
819
        }
17✔
820
    }
17✔
821

822
    void delete_user(SyncFileManager& file_manager, std::string_view user_id) override
823
    {
6✔
824
        std::lock_guard lock(m_mutex);
6✔
825
        if (auto it = m_users.find(user_id); it != m_users.end()) {
6✔
826
            m_users.erase(it);
5✔
827
        }
5✔
828
        if (auto it = m_realm_paths.find(user_id); it != m_realm_paths.end()) {
6✔
829
            for (auto& path : it->second) {
2✔
830
                file_manager.remove_realm(path);
2✔
831
            }
2✔
832
        }
1✔
833
    }
6✔
834

835
    std::string get_current_user() override
836
    {
1,839✔
837
        std::lock_guard lock(m_mutex);
1,839✔
838
        if (auto it = m_users.find(m_active_user); it != m_users.end() && it->second.access_token) {
1,839✔
839
            return m_active_user;
1,822✔
840
        }
1,822✔
841

842
        for (auto& [user_id, data] : m_users) {
17✔
843
            if (data.access_token) {
10✔
844
                m_active_user = user_id;
6✔
845
                return user_id;
6✔
846
            }
6✔
847
        }
10✔
848

849
        return "";
11✔
850
    }
17✔
851

852
    void set_current_user(std::string_view user_id) override
853
    {
1,853✔
854
        std::lock_guard lock(m_mutex);
1,853✔
855
        m_active_user = user_id;
1,853✔
856
    }
1,853✔
857

858
    std::vector<std::string> get_all_users() override
859
    {
30✔
860
        std::lock_guard lock(m_mutex);
30✔
861
        std::vector<std::string> users;
30✔
862
        for (auto& [user_id, _] : m_users) {
31✔
863
            users.push_back(user_id);
31✔
864
        }
31✔
865
        return users;
30✔
866
    }
30✔
867

868
    void add_realm_path(std::string_view user_id, std::string_view path) override
869
    {
18✔
870
        std::lock_guard lock(m_mutex);
18✔
871
        m_realm_paths[std::string(user_id)].insert(std::string(path));
18✔
872
    }
18✔
873

874
    bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view path) override
875
    {
16✔
876
        std::lock_guard lock(m_mutex);
16✔
877
        auto it = m_file_actions.find(path);
16✔
878
        if (it == m_file_actions.end())
16✔
879
            return false;
7✔
880
        auto& old_path = it->first;
9✔
881
        switch (it->second.action) {
9✔
882
            case SyncFileAction::DeleteRealm:
4✔
883
                if (file_manager.remove_realm(old_path)) {
4✔
884
                    m_file_actions.erase(it);
4✔
885
                    return true;
4✔
886
                }
4✔
NEW
887
                return false;
×
888

889
            case SyncFileAction::BackUpThenDeleteRealm:
5✔
890
                if (!util::File::exists(old_path)) {
5✔
891
                    m_file_actions.erase(it);
1✔
892
                    return true;
1✔
893
                }
1✔
894
                auto& new_path = it->second.backup_path;
4✔
895
                if (!file_manager.copy_realm_file(old_path, new_path)) {
4✔
896
                    return false;
1✔
897
                }
1✔
898
                if (file_manager.remove_realm(old_path)) {
3✔
899
                    m_file_actions.erase(it);
2✔
900
                    return true;
2✔
901
                }
2✔
902
                it->second.action = SyncFileAction::DeleteRealm;
1✔
903
                return false;
1✔
NEW
904
        }
×
NEW
905
        return false;
×
NEW
906
    }
×
907

908
    void create_file_action(SyncFileAction action, std::string_view path, std::string_view backup_path) override
909
    {
11✔
910
        std::lock_guard lock(m_mutex);
11✔
911
        REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !backup_path.empty());
11✔
912
        m_file_actions[std::string(path)] = FileAction{action, std::string(backup_path)};
11✔
913
    }
11✔
914
};
915

916
} // anonymous namespace
917

918
app::MetadataStore::~MetadataStore() = default;
2,191✔
919

920
std::unique_ptr<app::MetadataStore> app::create_metadata_store(const AppConfig& config, SyncFileManager& file_manager)
921
{
2,191✔
922
    if (config.metadata_mode == AppConfig::MetadataMode::InMemory) {
2,191✔
923
        return std::make_unique<InMemoryMetadataStorage>();
1,867✔
924
    }
1,867✔
925
    return std::make_unique<PersistedSyncMetadataManager>(file_manager.metadata_path(), config, file_manager);
324✔
926
}
324✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc