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

realm / realm-core / 2211

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

push

Evergreen

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

Rework sync user handling and metadata storage

102820 of 195548 branches covered (52.58%)

3165 of 3247 new or added lines in 46 files covered. (97.47%)

31 existing lines in 8 files now uncovered.

249584 of 269432 relevant lines covered (92.63%)

49986309.51 hits per line

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

97.28
/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
    {
627✔
49
        auto object_schema = realm.schema().find(table_name);
627✔
50
        table_key = object_schema->table_key;
627✔
51
        user_id = object_schema->persisted_properties[0].column_key;
627✔
52
    }
627✔
53

54
    static ObjectSchema object_schema()
55
    {
628✔
56
        return {table_name, {{table_name, PropertyType::String}}};
628✔
57
    }
628✔
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
    {
627✔
69
        auto object_schema = realm.schema().find(table_name);
627✔
70
        table_key = object_schema->table_key;
627✔
71
        user_id = object_schema->persisted_properties[0].column_key;
627✔
72
        provider_id = object_schema->persisted_properties[1].column_key;
627✔
73
    }
627✔
74

75
    static ObjectSchema object_schema()
76
    {
628✔
77
        return {table_name,
628✔
78
                ObjectSchema::ObjectType::Embedded,
628✔
79
                {
628✔
80
                    {"id", PropertyType::String},
628✔
81
                    {"provider_type", PropertyType::String},
628✔
82
                }};
628✔
83
    }
628✔
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
    {
633✔
114
        auto object_schema = realm.schema().find(table_name);
633✔
115
        table_key = object_schema->table_key;
633✔
116
        user_id_col = object_schema->persisted_properties[0].column_key;
633✔
117
        legacy_uuids_col = object_schema->persisted_properties[1].column_key;
633✔
118
        refresh_token_col = object_schema->persisted_properties[2].column_key;
633✔
119
        access_token_col = object_schema->persisted_properties[3].column_key;
633✔
120
        identities_col = object_schema->persisted_properties[4].column_key;
633✔
121
        state_col = object_schema->persisted_properties[5].column_key;
633✔
122
        device_id_col = object_schema->persisted_properties[6].column_key;
633✔
123
        profile_dump_col = object_schema->persisted_properties[7].column_key;
633✔
124
        realm_file_paths_col = object_schema->persisted_properties[8].column_key;
633✔
125
    }
633✔
126

127
    static ObjectSchema object_schema()
128
    {
628✔
129
        return {table_name,
628✔
130
                {{"identity", PropertyType::String},
628✔
131
                 {"legacy_uuids", PropertyType::String | PropertyType::Array},
628✔
132
                 {"refresh_token", PropertyType::String | PropertyType::Nullable},
628✔
133
                 {"access_token", PropertyType::String | PropertyType::Nullable},
628✔
134
                 {"identities", PropertyType::Object | PropertyType::Array, UserIdentitySchema::table_name},
628✔
135
                 {"state", PropertyType::Int},
628✔
136
                 {"device_id", PropertyType::String},
628✔
137
                 {"profile_data", PropertyType::String},
628✔
138
                 {"local_realm_paths", PropertyType::Set | PropertyType::String}}};
628✔
139
    }
628✔
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
    {
627✔
160
        auto object_schema = realm.schema().find(table_name);
627✔
161
        table_key = object_schema->table_key;
627✔
162
        idx_original_name = object_schema->persisted_properties[0].column_key;
627✔
163
        idx_new_name = object_schema->persisted_properties[1].column_key;
627✔
164
        idx_action = object_schema->persisted_properties[2].column_key;
627✔
165
        idx_partition = object_schema->persisted_properties[3].column_key;
627✔
166
        idx_user_identity = object_schema->persisted_properties[4].column_key;
627✔
167
    }
627✔
168

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

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

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

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

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

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

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

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

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

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

6✔
269
        // Finally we delete the duplicate object. We don't increment `i` as it's
6✔
270
        // now the index of the object just after the one we're deleting.
6✔
271
        obj.remove();
12✔
272
    }
12✔
273
}
6✔
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
{
628✔
287
    bool should_encrypt = app_config.metadata_mode == app::AppConfig::MetadataMode::Encryption;
628✔
288
    if (!REALM_PLATFORM_APPLE && should_encrypt && !app_config.custom_encryption_key)
628!
289
        throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided.");
1✔
290

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

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.
306
    auto key = keychain::get_existing_metadata_realm_key(app_config.app_id, app_config.security_access_group);
307
    if (key) {
×
308
        config.encryption_key = *key;
309
        if (auto realm = try_get_realm(config))
×
310
            return realm;
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.
316
    if (util::File::exists(config.path)) {
×
317
        config.encryption_key.clear();
318
        if (auto realm = try_get_realm(config))
×
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
323
        config.clear_on_invalid_file = true;
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.
329
    if (!key)
×
330
        key = keychain::create_new_metadata_realm_key(app_config.app_id, app_config.security_access_group);
331
    if (key)
×
332
        config.encryption_key = std::move(*key);
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
    {
628✔
348
        // Note that there are several deferred schema changes which don't
304✔
349
        // justify bumping the schema version by themself, but should be done
304✔
350
        // the next time something does justify a migration.
304✔
351
        // These include:
304✔
352
        // - remove FileActionSchema url and identity columns
304✔
353
        // - rename current_user_identity to CurrentUserId
304✔
354
        // - change most of the nullable columns to non-nullable
304✔
355
        constexpr uint64_t SCHEMA_VERSION = 7;
628✔
356

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

493
    std::optional<UserData> get_user(std::string_view user_id) override
494
    {
3,039✔
495
        auto realm = get_realm();
3,039✔
496
        return read_user(find_user(*realm, user_id));
3,039✔
497
    }
3,039✔
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
    {
1,113✔
502
        auto realm = get_realm();
1,113✔
503
        realm->begin_transaction();
1,113✔
504

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

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

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

545✔
521
        realm->commit_transaction();
1,113✔
522
    }
1,113✔
523

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

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

532✔
541
        auto identities_list = obj.get_linklist(schema.identities_col);
1,086✔
542
        identities_list.clear();
1,086✔
543

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

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

532✔
553
        realm->commit_transaction();
1,086✔
554
    }
1,086✔
555

556
    Obj current_user_obj(Realm& realm) const
557
    {
3,171✔
558
        TableRef current_user_table = realm.read_group().get_table(m_current_user_schema.table_key);
3,171✔
559
        Obj obj;
3,171✔
560
        if (!current_user_table->is_empty())
3,171✔
561
            obj = *current_user_table->begin();
2,593✔
562
        else if (realm.is_in_transaction())
578✔
563
            obj = current_user_table->create_object();
574✔
564
        return obj;
3,171✔
565
    }
3,171✔
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
    {
10,942✔
572
        auto str = obj.get<String>(col);
10,942✔
573
        return str.is_null() ? "" : str;
10,942✔
574
    }
10,942✔
575

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

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

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

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

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

1,479✔
619
        return data;
3,022✔
620
    }
3,022✔
621

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

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

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

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

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

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

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

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

18✔
708
        return "";
31✔
709
    }
36✔
710

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

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

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

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

3,106✔
741
        auto table = realm.read_group().get_table(m_user_schema.table_key);
6,347✔
742
        Query q = table->where().equal(m_user_schema.user_id_col, user_id);
6,347✔
743
        REALM_ASSERT_DEBUG(q.count() < 2); // user_id_col ought to be a primary key
6,347✔
744
        if (auto key = q.find())
6,347✔
745
            obj = table->get_object(key);
5,222✔
746
        return obj;
6,347✔
747
    }
6,347✔
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
    {
60✔
763
        std::lock_guard lock(m_mutex);
60✔
764
        auto it = m_users.find(user_id);
60✔
765
        return it != m_users.end() && it->second.access_token;
60✔
766
    }
60✔
767

768
    std::optional<UserData> get_user(std::string_view user_id) override
769
    {
11,058✔
770
        std::lock_guard lock(m_mutex);
11,058✔
771
        if (auto it = m_users.find(user_id); it != m_users.end()) {
11,058✔
772
            return it->second;
11,042✔
773
        }
11,042✔
774
        return {};
16✔
775
    }
16✔
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
    {
3,744✔
780
        std::lock_guard lock(m_mutex);
3,744✔
781
        auto it = m_users.find(user_id);
3,744✔
782
        if (it == m_users.end()) {
3,744✔
783
            it = m_users.insert({std::string(user_id), UserData{}}).first;
3,726✔
784
            m_active_user = user_id;
3,726✔
785
        }
3,726✔
786
        auto& user = it->second;
3,744✔
787
        user.device_id = device_id;
3,744✔
788
        try {
3,744✔
789
            user.refresh_token = RealmJWT(refresh_token);
3,744✔
790
            user.access_token = RealmJWT(access_token);
3,744✔
791
        }
3,744✔
792
        catch (...) {
1,880✔
793
            user.refresh_token = {};
16✔
794
            user.access_token = {};
16✔
795
        }
16✔
796
    }
3,744✔
797

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

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

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

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

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

17✔
849
        return "";
28✔
850
    }
34✔
851

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

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

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

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

889
            case SyncFileAction::BackUpThenDeleteRealm:
9✔
890
                if (!util::File::exists(old_path)) {
9✔
891
                    m_file_actions.erase(it);
2✔
892
                    return true;
2✔
893
                }
2✔
894
                auto& new_path = it->second.backup_path;
7✔
895
                if (!file_manager.copy_realm_file(old_path, new_path)) {
7✔
896
                    return false;
2✔
897
                }
2✔
898
                if (file_manager.remove_realm(old_path)) {
5✔
899
                    m_file_actions.erase(it);
4✔
900
                    return true;
4✔
901
                }
4✔
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
    {
21✔
910
        std::lock_guard lock(m_mutex);
21✔
911
        REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !backup_path.empty());
21✔
912
        m_file_actions[std::string(path)] = FileAction{action, std::string(backup_path)};
21✔
913
    }
21✔
914
};
915

916
} // anonymous namespace
917

918
app::MetadataStore::~MetadataStore() = default;
4,361✔
919

920
std::unique_ptr<app::MetadataStore> app::create_metadata_store(const AppConfig& config, SyncFileManager& file_manager)
921
{
4,361✔
922
    if (config.metadata_mode == AppConfig::MetadataMode::InMemory) {
4,361✔
923
        return std::make_unique<InMemoryMetadataStorage>();
3,733✔
924
    }
3,733✔
925
    return std::make_unique<PersistedSyncMetadataManager>(file_manager.metadata_path(), config, file_manager);
628✔
926
}
628✔
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