From 2f324886825002b486a975641efe0267ca446c0d Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 24 Jan 2024 21:27:11 -0800 Subject: [PATCH] Rework sync user handling and metadata storage This introduces the beginning of a split between sync and app services. Object store Sync types (almost) don't depend on the `app` namespace, and can be used independently of it. The SyncUser type now implements only the functionality required internally for sync, and is backed by a UserProvider interface which allows it to request things like access token refreshes. All user management has been removed from SyncManager, and it now only owns the sync client and active sync sessions. SyncSession is mostly unchanged. `app::User` is the new equivalent of the old SyncUser type. The user management which used to be in SyncManager is now in App, which implements the UserProvider interface. Metadata storage for sync and App has been completely redesigned. The metadata store is no longer optional, and instead has an in-memory implementation that should work identically to the persistent store other than not being persistent. The interface has been reworked to enable atomic updates to the metadata store rather than relying on the filesystem mutex in SyncManager, which will be required for multiprocess sync. This required pushing significantly more logic into the metadata storage, which fortunately turned out to also simplify things in the process. The ownership relationship between `App` and `User` has been inverted, with `App` now holding a weak cache of users and `User` strongly retaining the `App`. This ensures that a `SyncConfig` now retains everything it depends on even when app caching is not used. --- src/realm.h | 32 +- src/realm/mixed.hpp | 4 + src/realm/obj.hpp | 6 + src/realm/object-store/CMakeLists.txt | 37 +- src/realm/object-store/audit.hpp | 2 + src/realm/object-store/audit.mm | 47 +- src/realm/object-store/c_api/app.cpp | 60 +- src/realm/object-store/c_api/sync.cpp | 28 +- src/realm/object-store/c_api/types.hpp | 24 +- .../object-store/impl/realm_coordinator.cpp | 5 +- src/realm/object-store/sync/app.cpp | 507 ++++++----- src/realm/object-store/sync/app.hpp | 390 +++++---- src/realm/object-store/sync/app_config.hpp | 105 +++ .../object-store/sync/app_service_client.hpp | 8 +- src/realm/object-store/sync/app_user.cpp | 145 +++ src/realm/object-store/sync/app_user.hpp | 209 +++++ src/realm/object-store/sync/app_utils.cpp | 4 +- src/realm/object-store/sync/app_utils.hpp | 2 +- .../object-store/sync/auth_request_client.hpp | 4 +- .../object-store/sync/impl/app_metadata.cpp | 827 +++++++++++++++++ .../object-store/sync/impl/app_metadata.hpp | 80 ++ .../object-store/sync/impl/sync_client.hpp | 8 +- .../object-store/sync/impl/sync_file.cpp | 108 ++- .../object-store/sync/impl/sync_file.hpp | 42 +- .../object-store/sync/impl/sync_metadata.cpp | 828 ------------------ .../object-store/sync/impl/sync_metadata.hpp | 272 ------ src/realm/object-store/sync/mongo_client.hpp | 16 +- .../object-store/sync/mongo_collection.cpp | 4 +- .../object-store/sync/mongo_collection.hpp | 6 +- .../object-store/sync/mongo_database.cpp | 6 +- .../object-store/sync/mongo_database.hpp | 13 +- src/realm/object-store/sync/push_client.cpp | 4 +- src/realm/object-store/sync/push_client.hpp | 6 +- src/realm/object-store/sync/subscribable.hpp | 1 + src/realm/object-store/sync/sync_manager.cpp | 447 +--------- src/realm/object-store/sync/sync_manager.hpp | 198 +---- src/realm/object-store/sync/sync_session.cpp | 32 +- src/realm/object-store/sync/sync_session.hpp | 4 +- src/realm/object-store/sync/sync_user.cpp | 430 +++------ src/realm/object-store/sync/sync_user.hpp | 283 ++---- src/realm/object-store/sync/user_provider.hpp | 76 ++ src/realm/sync/socket_provider.hpp | 10 +- test/object-store/realm.cpp | 2 +- .../util/sync/sync_test_utils.hpp | 2 +- 44 files changed, 2444 insertions(+), 2880 deletions(-) create mode 100644 src/realm/object-store/sync/app_config.hpp create mode 100644 src/realm/object-store/sync/app_user.cpp create mode 100644 src/realm/object-store/sync/app_user.hpp create mode 100644 src/realm/object-store/sync/impl/app_metadata.cpp create mode 100644 src/realm/object-store/sync/impl/app_metadata.hpp delete mode 100644 src/realm/object-store/sync/impl/sync_metadata.cpp delete mode 100644 src/realm/object-store/sync/impl/sync_metadata.hpp create mode 100644 src/realm/object-store/sync/user_provider.hpp diff --git a/src/realm.h b/src/realm.h index 786a1a4976e..57362d86a79 100644 --- a/src/realm.h +++ b/src/realm.h @@ -2789,6 +2789,12 @@ typedef enum realm_auth_provider { RLM_AUTH_PROVIDER_API_KEY, } realm_auth_provider_e; +typedef enum realm_sync_client_metadata_mode { + RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT, + RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED, + RLM_SYNC_CLIENT_METADATA_MODE_DISABLED, +} realm_sync_client_metadata_mode_e; + typedef struct realm_app_user_apikey { realm_object_id_t id; const char* key; @@ -2897,6 +2903,12 @@ RLM_API void realm_app_config_set_framework_name(realm_app_config_t* config, RLM_API void realm_app_config_set_framework_version(realm_app_config_t* config, const char* framework_version) RLM_API_NOEXCEPT; RLM_API void realm_app_config_set_bundle_id(realm_app_config_t* config, const char* bundle_id) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_base_file_path(realm_app_config_t*, const char*) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_metadata_mode(realm_app_config_t*, + realm_sync_client_metadata_mode_e) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_metadata_encryption_key(realm_app_config_t*, const uint8_t[64]) RLM_API_NOEXCEPT; + +RLM_API realm_sync_client_config_t* realm_app_config_get_sync_client_config(realm_app_config_t*) RLM_API_NOEXCEPT; /** * Get an existing @a realm_app_credentials_t and return it's json representation @@ -2911,14 +2923,14 @@ RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credential * * @return A non-null pointer if no error occurred. */ -RLM_API realm_app_t* realm_app_create(const realm_app_config_t*, const realm_sync_client_config_t*); +RLM_API realm_app_t* realm_app_create(const realm_app_config_t*); /** * Create cached realm_app_t* instance given a valid realm configuration and sync client configuration. * * @return A non-null pointer if no error occurred. */ -RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t*, const realm_sync_client_config_t*); +RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t*); /** * Get a cached realm_app_t* instance given an app id. out_app may be null if the app with this id hasn't been @@ -3048,11 +3060,10 @@ RLM_API bool realm_app_link_user(realm_app_t* app, realm_user_t* user, realm_app * Switches the active user with the specified one. The user must exist in the list of all users who have logged into * this application. * @param app ptr to realm_app - * @param user ptr to current user - * @param new_user ptr to the new user to switch + * @param user ptr to user to set as current. * @return True if no error has been recorded, False otherwise */ -RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user, realm_user_t** new_user); +RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user); /** * Logs out and removes the provided user. @@ -3365,12 +3376,6 @@ RLM_API realm_app_t* realm_user_get_app(const realm_user_t*) RLM_API_NOEXCEPT; /* Sync */ -typedef enum realm_sync_client_metadata_mode { - RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT, - RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED, - RLM_SYNC_CLIENT_METADATA_MODE_DISABLED, -} realm_sync_client_metadata_mode_e; - typedef enum realm_sync_client_reconnect_mode { RLM_SYNC_CLIENT_RECONNECT_MODE_NORMAL, RLM_SYNC_CLIENT_RECONNECT_MODE_TESTING, @@ -3522,11 +3527,6 @@ typedef void (*realm_async_open_task_completion_func_t)(realm_userdata_t userdat typedef void (*realm_async_open_task_init_subscription_func_t)(realm_t* realm, realm_userdata_t userdata); RLM_API realm_sync_client_config_t* realm_sync_client_config_new(void) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_base_file_path(realm_sync_client_config_t*, const char*) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_metadata_mode(realm_sync_client_config_t*, - realm_sync_client_metadata_mode_e) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_metadata_encryption_key(realm_sync_client_config_t*, - const uint8_t[64]) RLM_API_NOEXCEPT; RLM_API void realm_sync_client_config_set_reconnect_mode(realm_sync_client_config_t*, realm_sync_client_reconnect_mode_e) RLM_API_NOEXCEPT; RLM_API void realm_sync_client_config_set_multiplex_sessions(realm_sync_client_config_t*, bool) RLM_API_NOEXCEPT; diff --git a/src/realm/mixed.hpp b/src/realm/mixed.hpp index 4d7378d28c8..836ee9fe243 100644 --- a/src/realm/mixed.hpp +++ b/src/realm/mixed.hpp @@ -167,6 +167,10 @@ class Mixed { : Mixed(StringData(s)) { } + Mixed(std::string_view s) noexcept + : Mixed(StringData(s)) + { + } DataType get_type() const noexcept { diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index 1f564599715..2b91b998eb8 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -509,6 +509,12 @@ inline Obj& Obj::set(ColKey col_key, std::string str, bool is_default) return set(col_key, StringData(str), is_default); } +template <> +inline Obj& Obj::set(ColKey col_key, std::string_view str, bool is_default) +{ + return set(col_key, StringData(str), is_default); +} + template <> inline Obj& Obj::set(ColKey col_key, realm::null, bool is_default) { diff --git a/src/realm/object-store/CMakeLists.txt b/src/realm/object-store/CMakeLists.txt index 083e8a10a7c..e8090d8187a 100644 --- a/src/realm/object-store/CMakeLists.txt +++ b/src/realm/object-store/CMakeLists.txt @@ -92,41 +92,46 @@ set(HEADERS if(REALM_ENABLE_SYNC) list(APPEND HEADERS sync/app.hpp - sync/app_utils.hpp + sync/app_config.hpp sync/app_credentials.hpp - sync/generic_network_transport.hpp - sync/async_open_task.hpp - sync/sync_manager.hpp - sync/sync_session.hpp - sync/sync_user.hpp sync/app_service_client.hpp + sync/app_user.hpp + sync/app_utils.hpp + sync/async_open_task.hpp sync/auth_request_client.hpp + sync/generic_network_transport.hpp sync/mongo_client.hpp sync/mongo_collection.hpp sync/mongo_database.hpp sync/push_client.hpp sync/subscribable.hpp + sync/sync_manager.hpp + sync/sync_session.hpp + sync/sync_user.hpp + sync/user_provider.hpp + sync/impl/app_metadata.hpp + sync/impl/network_reachability.hpp sync/impl/sync_client.hpp - sync/impl/sync_file.hpp - sync/impl/sync_metadata.hpp - sync/impl/network_reachability.hpp) + sync/impl/sync_file.hpp) list(APPEND SOURCES sync/app.cpp - sync/app_utils.cpp sync/app_credentials.cpp - sync/generic_network_transport.cpp + sync/app_user.cpp + sync/app_utils.cpp sync/async_open_task.cpp - sync/sync_manager.cpp - sync/sync_session.cpp - sync/sync_user.cpp + sync/generic_network_transport.cpp + sync/impl/app_metadata.cpp + sync/impl/sync_file.cpp sync/mongo_client.cpp sync/mongo_collection.cpp sync/mongo_database.cpp sync/push_client.cpp - sync/impl/sync_file.cpp - sync/impl/sync_metadata.cpp) + sync/sync_manager.cpp + sync/sync_session.cpp + sync/sync_user.cpp) + if(APPLE) list(APPEND HEADERS sync/impl/apple/network_reachability_observer.hpp diff --git a/src/realm/object-store/audit.hpp b/src/realm/object-store/audit.hpp index 94e8ea587b6..43e019d75ed 100644 --- a/src/realm/object-store/audit.hpp +++ b/src/realm/object-store/audit.hpp @@ -61,6 +61,8 @@ struct AuditConfig { // in the server-side schema for AuditEvent. This is not validated and will // result in a sync error if violated. std::vector> metadata; + // Root directory to store audit Realms + std::string_view base_file_path; }; class AuditInterface { diff --git a/src/realm/object-store/audit.mm b/src/realm/object-store/audit.mm index d46c2d521f0..c484cd44ce6 100644 --- a/src/realm/object-store/audit.mm +++ b/src/realm/object-store/audit.mm @@ -622,20 +622,15 @@ bool write_event(Timestamp timestamp, StringData activity, StringData event_type // Get a pool for the given sync user. Pools are cached internally to avoid // creating duplicate ones. - static std::shared_ptr get_pool(std::shared_ptr user, - std::string const& partition_prefix, - const std::shared_ptr& logger, - ErrorHandler error_handler); + static std::shared_ptr get_pool(std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger); // Write to a pooled Realm. The Transaction should not be retained outside // of the callback. void write(util::FunctionRef func) REQUIRES(!m_mutex); - explicit AuditRealmPool(Private, std::shared_ptr user, std::string const& partition_prefix, - ErrorHandler error_handler, const std::shared_ptr& logger, - std::string_view app_id); - AuditRealmPool(const AuditRealmPool&) = delete; - AuditRealmPool& operator=(const AuditRealmPool&) = delete; + explicit AuditRealmPool(Private, std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger); // Block the calling thread until all pooled Realms have been fully uploaded, // including ones which do not currently have sync sessions. For testing @@ -663,13 +658,12 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con std::string prefixed_partition(std::string const& partition); }; -std::shared_ptr AuditRealmPool::get_pool(std::shared_ptr user, - std::string const& partition_prefix, - const std::shared_ptr& logger, - ErrorHandler error_handler) NO_THREAD_SAFETY_ANALYSIS +std::shared_ptr +AuditRealmPool::get_pool(std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger) NO_THREAD_SAFETY_ANALYSIS { struct CachedPool { - std::string user_identity; + std::string user_id; std::string partition_prefix; std::string app_id; std::weak_ptr pool; @@ -683,9 +677,9 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con }), s_pools.end()); - auto app_id = user->sync_manager()->app().lock()->config().app_id; + auto app_id = user->app_id(); auto it = std::find_if(s_pools.begin(), s_pools.end(), [&](auto& pool) { - return pool.user_identity == user->identity() && pool.partition_prefix == partition_prefix && + return pool.user_id == user->user_id() && pool.partition_prefix == config.partition_value_prefix && pool.app_id == app_id; }); if (it != s_pools.end()) { @@ -694,28 +688,26 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con } } - auto pool = std::make_shared(Private(), user, partition_prefix, error_handler, logger, app_id); + auto pool = std::make_shared(Private(), user, config, logger); pool->scan_for_realms_to_upload(); - s_pools.push_back({user->identity(), partition_prefix, app_id, pool}); + s_pools.push_back({user->user_id(), config.partition_value_prefix, app_id, pool}); return pool; } -AuditRealmPool::AuditRealmPool(Private, std::shared_ptr user, std::string const& partition_prefix, - ErrorHandler error_handler, const std::shared_ptr& logger, - std::string_view app_id) +AuditRealmPool::AuditRealmPool(Private, std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger) : m_user(user) - , m_partition_prefix(partition_prefix) - , m_error_handler(error_handler) + , m_partition_prefix(config.partition_value_prefix) + , m_error_handler(config.sync_error_handler) , m_path_root([&] { - auto base_file_path = m_user->sync_manager()->config().base_file_path; #ifdef _WIN32 // Move to File? const char separator[] = "\\"; #else const char separator[] = "/"; #endif // "$root/realm-audit/$appId/$userId/$partitonPrefix/" - return util::format("%2%1realm-audit%1%3%1%4%1%5%1", separator, base_file_path, app_id, m_user->identity(), - partition_prefix); + return util::format("%2%1realm-audit%1%3%1%4%1%5%1", separator, config.base_file_path, m_user->app_id(), + m_user->user_id(), config.partition_value_prefix); }()) , m_logger(logger) { @@ -1016,8 +1008,7 @@ throw InvalidArgument("Auditing a flexible sync realm requires setting the audit if (!m_serializer) m_serializer = std::make_shared(); - m_realm_pool = AuditRealmPool::get_pool(audit_user, audit_config.partition_value_prefix, m_logger, - audit_config.sync_error_handler); + m_realm_pool = AuditRealmPool::get_pool(audit_user, audit_config, m_logger); } void AuditContext::update_metadata(std::vector> new_metadata) diff --git a/src/realm/object-store/c_api/app.cpp b/src/realm/object-store/c_api/app.cpp index 92fb619c0e6..b1566fd40dc 100644 --- a/src/realm/object-store/c_api/app.cpp +++ b/src/realm/object-store/c_api/app.cpp @@ -27,9 +27,9 @@ namespace realm::c_api { using namespace realm::app; -static_assert(realm_user_state_e(SyncUser::State::LoggedOut) == RLM_USER_STATE_LOGGED_OUT); -static_assert(realm_user_state_e(SyncUser::State::LoggedIn) == RLM_USER_STATE_LOGGED_IN); -static_assert(realm_user_state_e(SyncUser::State::Removed) == RLM_USER_STATE_REMOVED); +static_assert(realm_user_state_e(UserState::LoggedOut) == RLM_USER_STATE_LOGGED_OUT); +static_assert(realm_user_state_e(UserState::LoggedIn) == RLM_USER_STATE_LOGGED_IN); +static_assert(realm_user_state_e(UserState::Removed) == RLM_USER_STATE_REMOVED); static_assert(realm_auth_provider_e(AuthProvider::ANONYMOUS) == RLM_AUTH_PROVIDER_ANONYMOUS); static_assert(realm_auth_provider_e(AuthProvider::ANONYMOUS_NO_REUSE) == RLM_AUTH_PROVIDER_ANONYMOUS_NO_REUSE); @@ -86,7 +86,7 @@ static inline auto make_callback(realm_app_user_completion_func_t callback, real realm_free_userdata_func_t userdata_free) { return [callback, userdata = SharedUserdata(userdata, FreeUserdata(userdata_free))]( - std::shared_ptr user, util::Optional error) { + std::shared_ptr user, util::Optional error) { if (error) { realm_app_error_t c_err{to_capi(*error)}; callback(userdata.get(), nullptr, &c_err); @@ -238,6 +238,22 @@ RLM_API void realm_app_config_set_bundle_id(realm_app_config_t* config, const ch config->device_info.bundle_id = std::string(bundle_id); } +RLM_API void realm_app_config_set_base_file_path(realm_app_config_t* config, const char* path) noexcept +{ + config->base_file_path = path; +} + +RLM_API void realm_app_config_set_metadata_mode(realm_app_config_t* config, + realm_sync_client_metadata_mode_e mode) noexcept +{ + config->metadata_mode = app::AppConfig::MetadataMode(mode); +} + +RLM_API void realm_app_config_set_metadata_encryption_key(realm_app_config_t* config, const uint8_t key[64]) noexcept +{ + config->custom_encryption_key = std::vector(key, key + 64); +} + RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credentials_t* app_credentials) noexcept { return wrap_err([&] { @@ -245,19 +261,17 @@ RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credential }); } -RLM_API realm_app_t* realm_app_create(const realm_app_config_t* app_config, - const realm_sync_client_config_t* sync_client_config) +RLM_API realm_app_t* realm_app_create(const realm_app_config_t* app_config) { return wrap_err([&] { - return new realm_app_t(App::get_app(app::App::CacheMode::Disabled, *app_config, *sync_client_config)); + return new realm_app_t(App::get_app(app::App::CacheMode::Disabled, *app_config)); }); } -RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t* app_config, - const realm_sync_client_config_t* sync_client_config) +RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t* app_config) { return wrap_err([&] { - return new realm_app_t(App::get_app(app::App::CacheMode::Enabled, *app_config, *sync_client_config)); + return new realm_app_t(App::get_app(app::App::CacheMode::Enabled, *app_config)); }); } @@ -377,13 +391,10 @@ RLM_API bool realm_app_link_user(realm_app_t* app, realm_user_t* user, realm_app }); } -RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user, realm_user_t** new_user) +RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user) { return wrap_err([&] { - auto new_user_local = (*app)->switch_user(*user); - if (new_user) { - *new_user = new realm_user_t(std::move(new_user_local)); - } + (*app)->switch_user(*user); return true; }); } @@ -649,17 +660,21 @@ RLM_API void realm_app_sync_client_wait_for_sessions_to_terminate(realm_app_t* a RLM_API char* realm_app_sync_client_get_default_file_path_for_realm(const realm_sync_config_t* config, const char* custom_filename) { - return wrap_err([&]() { + return wrap_err([&]() -> char* { + auto user = std::dynamic_pointer_cast(config->user); + if (!user) { + return nullptr; + } util::Optional filename = custom_filename ? util::some(custom_filename) : util::none; - std::string file_path = config->user->sync_manager()->path_for_realm(*config, std::move(filename)); + std::string file_path = user->app()->path_for_realm(*config, std::move(filename)); return duplicate_string(file_path); }); } RLM_API const char* realm_user_get_identity(const realm_user_t* user) noexcept { - return (*user)->identity().c_str(); + return (*user)->user_id().c_str(); } RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) noexcept @@ -740,14 +755,7 @@ RLM_API char* realm_user_get_refresh_token(const realm_user_t* user) RLM_API realm_app_t* realm_user_get_app(const realm_user_t* user) noexcept { REALM_ASSERT(user); - try { - if (auto shared_app = (*user)->sync_manager()->app().lock()) { - return new realm_app_t(shared_app); - } - } - catch (const std::exception&) { - } - return nullptr; + return new realm_app_t((*user)->app()); } template diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 3813336363c..d61adf2d113 100644 --- a/src/realm/object-store/c_api/sync.cpp +++ b/src/realm/object-store/c_api/sync.cpp @@ -42,11 +42,11 @@ realm_sync_session_connection_state_notification_token::~realm_sync_session_conn namespace realm::c_api { -static_assert(realm_sync_client_metadata_mode_e(SyncClientConfig::MetadataMode::NoEncryption) == +static_assert(realm_sync_client_metadata_mode_e(app::AppConfig::MetadataMode::NoEncryption) == RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT); -static_assert(realm_sync_client_metadata_mode_e(SyncClientConfig::MetadataMode::Encryption) == +static_assert(realm_sync_client_metadata_mode_e(app::AppConfig::MetadataMode::Encryption) == RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED); -static_assert(realm_sync_client_metadata_mode_e(SyncClientConfig::MetadataMode::NoMetadata) == +static_assert(realm_sync_client_metadata_mode_e(app::AppConfig::MetadataMode::InMemory) == RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); static_assert(realm_sync_client_reconnect_mode_e(ReconnectMode::normal) == RLM_SYNC_CLIENT_RECONNECT_MODE_NORMAL); @@ -136,24 +136,6 @@ RLM_API realm_sync_client_config_t* realm_sync_client_config_new(void) noexcept return new realm_sync_client_config_t; } -RLM_API void realm_sync_client_config_set_base_file_path(realm_sync_client_config_t* config, - const char* path) noexcept -{ - config->base_file_path = path; -} - -RLM_API void realm_sync_client_config_set_metadata_mode(realm_sync_client_config_t* config, - realm_sync_client_metadata_mode_e mode) noexcept -{ - config->metadata_mode = SyncClientConfig::MetadataMode(mode); -} - -RLM_API void realm_sync_client_config_set_metadata_encryption_key(realm_sync_client_config_t* config, - const uint8_t key[64]) noexcept -{ - config->custom_encryption_key = std::vector(key, key + 64); -} - RLM_API void realm_sync_client_config_set_reconnect_mode(realm_sync_client_config_t* config, realm_sync_client_reconnect_mode_e mode) noexcept { @@ -736,7 +718,7 @@ realm_sync_session_get_connection_state(const realm_sync_session_t* session) noe RLM_API realm_user_t* realm_sync_session_get_user(const realm_sync_session_t* session) noexcept { - return new realm_user_t((*session)->user()); + return new realm_user_t(std::static_pointer_cast((*session)->user())); } RLM_API const char* realm_sync_session_get_partition_value(const realm_sync_session_t* session) noexcept @@ -763,7 +745,7 @@ RLM_API bool realm_sync_immediately_run_file_actions(realm_app_t* realm_app, con bool* did_run) noexcept { return wrap_err([&]() { - *did_run = (*realm_app)->sync_manager()->immediately_run_file_actions(sync_path); + *did_run = (*realm_app)->immediately_run_file_actions(sync_path); return true; }); } diff --git a/src/realm/object-store/c_api/types.hpp b/src/realm/object-store/c_api/types.hpp index 0c7f3f0a096..97b1ce3d588 100644 --- a/src/realm/object-store/c_api/types.hpp +++ b/src/realm/object-store/c_api/types.hpp @@ -1,30 +1,30 @@ #ifndef REALM_OBJECT_STORE_C_API_TYPES_HPP #define REALM_OBJECT_STORE_C_API_TYPES_HPP -#include #include -#include -#include #include -#include -#include +#include +#include #include #include -#include +#include +#include #include +#include #if REALM_ENABLE_SYNC #include +#include #include -#include #include #include #include #include #endif +#include #include #include @@ -612,8 +612,8 @@ struct realm_http_transport : realm::c_api::WrapC, std::shared_ptr { - realm_user(std::shared_ptr user) - : std::shared_ptr{std::move(user)} +struct realm_user : realm::c_api::WrapC, std::shared_ptr { + realm_user(std::shared_ptr user) + : std::shared_ptr{std::move(user)} { } diff --git a/src/realm/object-store/impl/realm_coordinator.cpp b/src/realm/object-store/impl/realm_coordinator.cpp index f0c29d1997c..000b5167208 100644 --- a/src/realm/object-store/impl/realm_coordinator.cpp +++ b/src/realm/object-store/impl/realm_coordinator.cpp @@ -32,7 +32,6 @@ #include #if REALM_ENABLE_SYNC -#include #include #include #include @@ -436,7 +435,9 @@ bool RealmCoordinator::open_db() // If we previously opened this Realm, we may have a lingering sync // session which outlived its RealmCoordinator. If that happens we // want to reuse it instead of creating a new DB. - m_sync_session = m_config.sync_config->user->sync_manager()->get_existing_session(m_config.path); + if (auto sync_manager = m_config.sync_config->user->sync_manager()) { + m_sync_session = sync_manager->get_existing_session(m_config.path); + } if (m_sync_session) { m_db = SyncSession::Internal::get_db(*m_sync_session); init_external_helpers(); diff --git a/src/realm/object-store/sync/app.cpp b/src/realm/object-store/sync/app.cpp index 56cf1e5315e..5b2289c82cb 100644 --- a/src/realm/object-store/sync/app.cpp +++ b/src/realm/object-store/sync/app.cpp @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////// -#include "external/json/json.hpp" #include #include @@ -24,7 +23,10 @@ #include #include #include +#include #include +#include +#include #include #include @@ -32,6 +34,7 @@ #include #endif +#include #include #include @@ -142,7 +145,7 @@ enum class RequestTokenType { NoAuth, AccessToken, RefreshToken }; // generate the request headers for a HTTP call, by default it will generate headers with a refresh token if a user is // passed -HttpHeaders get_request_headers(const std::shared_ptr& with_user_authorization = nullptr, +HttpHeaders get_request_headers(const std::shared_ptr& with_user_authorization = nullptr, RequestTokenType token_type = RequestTokenType::RefreshToken) { HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}}; @@ -181,59 +184,30 @@ constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys"; constexpr static int s_max_http_redirects = 20; static util::FlatMap> s_apps_cache; // app_id -> base_url -> app std::mutex s_apps_mutex; - } // anonymous namespace namespace realm { namespace app { - -App::Config::DeviceInfo::DeviceInfo() - : platform(util::get_library_platform()) - , cpu_arch(util::get_library_cpu_arch()) - , core_version(REALM_VERSION_STRING) -{ -} - -App::Config::DeviceInfo::DeviceInfo(std::string a_platform_version, std::string an_sdk_version, std::string an_sdk, - std::string a_device_name, std::string a_device_version, - std::string a_framework_name, std::string a_framework_version, - std::string a_bundle_id) - : DeviceInfo() -{ - platform_version = a_platform_version; - sdk_version = an_sdk_version; - sdk = an_sdk; - device_name = a_device_name; - device_version = a_device_version; - framework_name = a_framework_name; - framework_version = a_framework_version; - bundle_id = a_bundle_id; -} - // NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive. // "Calling function configure requires negative capability '!app->m_route_mutex'" // But 'app' is an object just created in this static method so it is not possible to annotate this in the header. -SharedApp App::get_app(CacheMode mode, const Config& config, - const SyncClientConfig& sync_client_config) NO_THREAD_SAFETY_ANALYSIS +SharedApp App::get_app(CacheMode mode, const AppConfig& config) NO_THREAD_SAFETY_ANALYSIS { if (mode == CacheMode::Enabled) { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(s_default_base_url))]; if (!app) { app = std::make_shared(Private(), config); - app->configure(sync_client_config); } return app; } REALM_ASSERT(mode == CacheMode::Disabled); - auto app = std::make_shared(Private(), config); - app->configure(sync_client_config); - return app; + return std::make_shared(Private(), config); } SharedApp App::get_cached_app(const std::string& app_id, const std::optional& base_url) { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); if (auto it = s_apps_cache.find(app_id); it != s_apps_cache.end()) { const auto& apps_by_url = it->second; @@ -248,13 +222,13 @@ SharedApp App::get_cached_app(const std::string& app_id, const std::optional lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); s_apps_cache.clear(); } void App::close_all_sync_sessions() { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); for (auto& apps_by_url : s_apps_cache) { for (auto& app : apps_by_url.second) { app.second->sync_manager()->close_all_sessions(); @@ -262,11 +236,13 @@ void App::close_all_sync_sessions() } } -App::App(Private, const Config& config) +App::App(Private, const AppConfig& config) : m_config(config) , m_base_url(m_config.base_url.value_or(std::string(s_default_base_url))) - , m_location_updated(false) , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms)) + , m_file_manager(std::make_unique(config.base_file_path, config.app_id)) + , m_metadata_store(create_metadata_store(config, *m_file_manager)) + , m_sync_manager(std::make_shared(config.sync_client_config)) { #ifdef __EMSCRIPTEN__ if (!m_config.transport) { @@ -274,7 +250,6 @@ App::App(Private, const Config& config) } #endif REALM_ASSERT(m_config.transport); - REALM_ASSERT(!m_config.device_info.platform.empty()); // if a base url is provided, then verify the value if (m_config.base_url) { @@ -285,7 +260,7 @@ App::App(Private, const Config& config) // Setup a baseline set of routes using the provided or default base url // These will be updated when the location info is refreshed prior to sending the // first AppServices HTTP request. - configure_route(m_base_url); + configure_route(m_base_url, m_base_url); if (m_config.device_info.platform_version.empty()) { throw InvalidArgument("You must specify the Platform Version in App::Config::device_info"); @@ -302,25 +277,13 @@ App::App(Private, const Config& config) App::~App() {} -void App::configure(const SyncClientConfig& sync_client_config) -{ - { - util::CheckedLockGuard guard(m_route_mutex); - // Make sure to request the location when the app is configured - m_location_updated = false; - } - - // Start with an empty sync route in the sync manager. It will ensure the - // location has been updated at least once when the first sync session is - // started by requesting a new access token. - m_sync_manager = std::make_shared(); - m_sync_manager->configure(shared_from_this(), util::none, sync_client_config); -} - bool App::init_logger() { - if (!m_logger_ptr && m_sync_manager) { + if (!m_logger_ptr) { m_logger_ptr = m_sync_manager->get_logger(); + if (!m_logger_ptr) { + m_logger_ptr = util::Logger::get_default_logger(); + } } return bool(m_logger_ptr); } @@ -370,28 +333,19 @@ std::string App::get_ws_host_url() return m_ws_host_url; } - std::string App::make_sync_route(Optional ws_host_url) { return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id, s_sync_path); } -void App::configure_route(const std::string& host_url, const std::optional& ws_host_url) +void App::configure_route(const std::string& host_url, const std::string& ws_host_url) { - // We got a new host url, save it - m_host_url = (host_url.length() > 0 ? host_url : m_base_url); + m_host_url = host_url; + m_ws_host_url = ws_host_url; - // If a valid websocket host url was included, save it - if (ws_host_url && ws_host_url->length() > 0) { - m_ws_host_url = *ws_host_url; - } - // Otherwise, convert the host url to a websocket host url (http[s]:// -> ws[s]://) - else { - m_ws_host_url = m_host_url; - if (m_ws_host_url.find("http") == 0) { - m_ws_host_url.replace(0, 4, "ws"); - } + if (m_ws_host_url.find("http") == 0) { + m_ws_host_url.replace(0, 4, "ws"); } // host_url is the url to the server: e.g., https://realm.mongodb.com or https://localhost:9090 @@ -415,7 +369,7 @@ void App::update_hostname(const std::string& host_url, const std::optional 0 ? host_url : m_base_url, ws_host_url); + configure_route(host_url.length() > 0 ? host_url : m_base_url, ws_host_url.value_or(host_url)); } // MARK: - Template specializations @@ -509,7 +463,7 @@ std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path } void App::UserAPIKeyProviderClient::create_api_key( - const std::string& name, const std::shared_ptr& user, + const std::string& name, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -521,7 +475,7 @@ void App::UserAPIKeyProviderClient::create_api_key( UserAPIKeyResponseHandler{std::move(completion)}); } -void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -533,7 +487,7 @@ void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, con } void App::UserAPIKeyProviderClient::fetch_api_keys( - const std::shared_ptr& user, + const std::shared_ptr& user, UniqueFunction&&, Optional)>&& completion) { Request req; @@ -562,7 +516,7 @@ void App::UserAPIKeyProviderClient::fetch_api_keys( }); } -void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -573,7 +527,7 @@ void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, co handle_default_response(std::move(completion))); } -void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -584,7 +538,7 @@ void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, co handle_default_response(std::move(completion))); } -void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -596,14 +550,44 @@ void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, c } // MARK: - App -std::shared_ptr App::current_user() const +std::shared_ptr App::get_user_for_id(const std::string& user_id) { - return m_sync_manager->get_current_user(); + if (auto& user = m_users[user_id]; user && user->is_logged_in()) { + return user->shared_from_this(); + } + return User::make(shared_from_this(), user_id); } -std::vector> App::all_users() const +void App::user_data_updated(const std::string& user_id) { - return m_sync_manager->all_users(); + if (auto it = m_users.find(user_id); it != m_users.end()) { + it->second->update_backing_data(m_metadata_store->get_user(user_id)); + } +} + +std::shared_ptr App::current_user() +{ + util::CheckedLockGuard lock(m_user_mutex); + if (m_current_user && m_current_user->is_logged_in()) + return m_current_user->shared_from_this(); + if (auto user_id = m_metadata_store->get_current_user(); !user_id.empty()) { + auto user = get_user_for_id(user_id); + m_current_user = user.get(); + return user; + } + return nullptr; +} + +std::shared_ptr App::get_existing_logged_in_user(std::string_view user_id) +{ + util::CheckedLockGuard lock(m_user_mutex); + if (auto it = m_users.find(std::string(user_id)); it != m_users.end() && it->second->is_logged_in()) { + return it->second->shared_from_this(); + } + if (m_metadata_store->has_user(user_id)) { + return User::make(shared_from_this(), user_id); + } + return nullptr; } std::string App::get_base_url() const @@ -648,8 +632,20 @@ void App::update_base_url(std::optional base_url, UniqueFunction& sync_user, - UniqueFunction&, Optional)>&& completion) +std::vector> App::all_users() +{ + util::CheckedLockGuard lock(m_user_mutex); + auto user_ids = m_metadata_store->get_logged_in_users(); + std::vector> users; + users.reserve(user_ids.size()); + for (auto& user_id : user_ids) { + users.push_back(get_user_for_id(user_id)); + } + return users; +} + +void App::get_profile(const std::shared_ptr& user, + UniqueFunction&, Optional)>&& completion) { Request req; req.method = HttpMethod::get; @@ -658,8 +654,9 @@ void App::get_profile(const std::shared_ptr& sync_user, req.uses_refresh_token = false; do_authenticated_request( - std::move(req), sync_user, - [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) { + std::move(req), user, + [completion = std::move(completion), self = shared_from_this(), user, + this](const Response& profile_response) { if (auto error = AppUtils::check_for_errors(profile_response)) { return completion(nullptr, std::move(error)); } @@ -668,24 +665,24 @@ void App::get_profile(const std::shared_ptr& sync_user, auto profile_json = parse(profile_response.body); auto identities_json = get(profile_json, "identities"); - std::vector identities; - identities.reserve(profile_json.size()); + std::vector identities; + identities.reserve(identities_json.size()); for (auto& identity_json : identities_json) { auto doc = as(identity_json); - identities.push_back( - SyncUserIdentity(get(doc, "id"), get(doc, "provider_type"))); + identities.push_back({get(doc, "id"), get(doc, "provider_type")}); } - sync_user->update_user_profile(std::move(identities), - SyncUserProfile(get(profile_json, "data"))); - self->m_sync_manager->set_current_user(sync_user->identity()); - self->emit_change_to_subscribers(*self); + auto data = m_metadata_store->get_user(user->user_id()); + data.second.identities = std::move(identities); + data.second.profile = UserProfile(get(profile_json, "data")); + m_metadata_store->update_user(user->user_id(), data.first, data.second); + user->update_backing_data(std::move(data)); } catch (const AppError& err) { return completion(nullptr, err); } - return completion(sync_user, {}); + return completion(user, {}); }); } @@ -693,28 +690,27 @@ void App::attach_auth_options(BsonDocument& body) { BsonDocument options; - log_debug("App: version info: platform: %1 version: %2 - sdk: %3 - sdk version: %4 - core version: %5", - m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk, - m_config.device_info.sdk_version, m_config.device_info.core_version); + // log_debug("App: version info: platform: %1 version: %2 - sdk: %3 - sdk version: %4 - core version: %5", + // m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk, + // m_config.device_info.sdk_version, m_config.device_info.core_version); options["appId"] = m_config.app_id; - options["platform"] = m_config.device_info.platform; + options["platform"] = util::get_library_platform(); options["platformVersion"] = m_config.device_info.platform_version; options["sdk"] = m_config.device_info.sdk; options["sdkVersion"] = m_config.device_info.sdk_version; - options["cpuArch"] = m_config.device_info.cpu_arch; + options["cpuArch"] = util::get_library_cpu_arch(); options["deviceName"] = m_config.device_info.device_name; options["deviceVersion"] = m_config.device_info.device_version; options["frameworkName"] = m_config.device_info.framework_name; options["frameworkVersion"] = m_config.device_info.framework_version; - options["coreVersion"] = m_config.device_info.core_version; + options["coreVersion"] = REALM_VERSION_STRING; options["bundleId"] = m_config.device_info.bundle_id; body["options"] = BsonDocument({{"device", options}}); } -void App::log_in_with_credentials( - const AppCredentials& credentials, const std::shared_ptr& linking_user, - UniqueFunction&, Optional)>&& completion) +void App::log_in_with_credentials(const AppCredentials& credentials, const std::shared_ptr& linking_user, + UniqueFunction&, Optional)>&& completion) { if (would_log(util::Logger::Level::debug)) { auto app_info = util::format("app_id: %1", m_config.app_id); @@ -722,15 +718,32 @@ void App::log_in_with_credentials( } // if we try logging in with an anonymous user while there // is already an anonymous session active, reuse it + std::shared_ptr anon_user; if (credentials.provider() == AuthProvider::ANONYMOUS) { - for (auto&& user : m_sync_manager->all_users()) { + util::CheckedLockGuard lock(m_user_mutex); + for (auto& [_, user] : m_users) { if (user->is_anonymous()) { - completion(switch_user(user), util::none); - return; + anon_user = user->shared_from_this(); + m_current_user = user; + m_metadata_store->set_current_user(user->user_id()); + break; } } } + if (anon_user) { + emit_change_to_subscribers(*this); + completion(anon_user, util::none); + return; + } + + if (linking_user) { + util::CheckedLockGuard lock(m_user_mutex); + if (!verify_user_present(linking_user)) { + return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found.")); + } + } + // construct the route std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(), linking_user ? "?link=true" : ""); @@ -741,60 +754,73 @@ void App::log_in_with_credentials( do_request( {HttpMethod::post, route, m_request_timeout_ms, get_request_headers(linking_user, RequestTokenType::AccessToken), Bson(body).to_string()}, - [completion = std::move(completion), credentials, linking_user, - self = shared_from_this()](const Response& response) mutable { + [completion = std::move(completion), credentials, linking_user, self = shared_from_this(), + this](const Response& response) mutable { if (auto error = AppUtils::check_for_errors(response)) { - self->log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code, - error->what()); + log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code, + error->what()); return completion(nullptr, std::move(error)); } - std::shared_ptr sync_user = linking_user; + std::shared_ptr sync_user = linking_user; try { auto json = parse(response.body); if (linking_user) { - linking_user->update_access_token(get(json, "access_token")); + auto user_data = m_metadata_store->get_user(linking_user->user_id()); + user_data.first.access_token = RealmJWT(get(json, "access_token")); + // maybe a callback for this? + m_metadata_store->update_user(linking_user->user_id(), user_data.first, user_data.second); + linking_user->update_backing_data(std::move(user_data)); } else { - sync_user = self->m_sync_manager->get_user( - get(json, "user_id"), get(json, "refresh_token"), - get(json, "access_token"), get(json, "device_id")); + auto user_id = get(json, "user_id"); + m_metadata_store->create_user(user_id, get(json, "refresh_token"), + get(json, "access_token"), + get(json, "device_id")); + util::CheckedLockGuard lock(m_user_mutex); + user_data_updated(user_id); // FIXME: needs to be callback from metadata store + sync_user = get_user_for_id(user_id); } } catch (const AppError& e) { return completion(nullptr, e); } - self->get_profile(sync_user, std::move(completion)); + switch_user(sync_user); + get_profile(sync_user, std::move(completion)); }, false); } void App::log_in_with_credentials( const AppCredentials& credentials, - util::UniqueFunction&, Optional)>&& completion) + util::UniqueFunction&, Optional)>&& completion) { App::log_in_with_credentials(credentials, nullptr, std::move(completion)); } -void App::log_out(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::log_out(const std::shared_ptr& user, UserState new_state, + UniqueFunction)>&& completion) { - if (!user || user->state() != SyncUser::State::LoggedIn) { - log_debug("App: log_out() - already logged out"); + if (!user || user->state() == new_state) { return completion(util::none); } - log_debug("App: log_out(%1)", user->user_profile().name()); - auto refresh_token = user->refresh_token(); - user->log_out(); - Request req; req.method = HttpMethod::del; req.url = url_for_path("/auth/session"); req.timeout_ms = m_request_timeout_ms; req.uses_refresh_token = true; - req.headers = get_request_headers(); - req.headers.insert({"Authorization", util::format("Bearer %1", refresh_token)}); + req.headers = get_request_headers(user); + + SyncUserData data; + data.state = new_state; + user->SyncUser::update_backing_data(std::move(data)); + m_metadata_store->log_out(user->user_id(), new_state); + + if (!user->is_logged_in()) { + return completion(util::none); + } do_request(std::move(req), [self = shared_from_this(), completion = std::move(completion)](const Response& response) { @@ -806,39 +832,55 @@ void App::log_out(const std::shared_ptr& user, UniqueFunction& user, UniqueFunction)>&& completion) +{ + auto new_state = user && user->is_anonymous() ? UserState::Removed : UserState::LoggedOut; + log_out(user, new_state, std::move(completion)); +} + void App::log_out(UniqueFunction)>&& completion) { - log_debug("App: log_out(current user)"); - log_out(m_sync_manager->get_current_user(), std::move(completion)); + log_out(current_user(), std::move(completion)); } -bool App::verify_user_present(const std::shared_ptr& user) const +bool App::verify_user_present(const std::shared_ptr& user) const { - auto users = m_sync_manager->all_users(); - return std::any_of(users.begin(), users.end(), [&](auto&& u) { - return u == user; - }); + for (auto& [_, u] : m_users) { + if (u == user.get()) + return true; + } + return false; } -std::shared_ptr App::switch_user(const std::shared_ptr& user) const +void App::switch_user(const std::shared_ptr& user) { - if (!user || user->state() != SyncUser::State::LoggedIn) { + if (!user || user->state() != UserState::LoggedIn) { throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out"); } + util::CheckedLockGuard lock(m_user_mutex); if (!verify_user_present(user)) { throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist"); } - m_sync_manager->set_current_user(user->identity()); + m_current_user = user.get(); + m_metadata_store->set_current_user(user->user_id()); emit_change_to_subscribers(*this); - return m_sync_manager->get_current_user(); } -void App::remove_user(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::do_remove_user(SyncUser& user) { - if (!user || user->state() == SyncUser::State::Removed) { + MetadataStore::Data data; + data.first.state = UserState::Removed; + user.update_backing_data(std::move(data.first)); +} + +void App::remove_user(const std::shared_ptr& user, UniqueFunction)>&& completion) +{ + if (!user || user->state() == UserState::Removed) { return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed")); } + + util::CheckedLockGuard lock(m_user_mutex); if (!verify_user_present(user)) { return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found")); } @@ -846,25 +888,25 @@ void App::remove_user(const std::shared_ptr& user, UniqueFunctionis_logged_in()) { log_out(user, [user, completion = std::move(completion), self = shared_from_this()](const Optional& error) { - self->m_sync_manager->remove_user(user->identity()); + self->do_remove_user(*user); return completion(error); }); } else { - m_sync_manager->remove_user(user->identity()); - return completion({}); + do_remove_user(*user); } } -void App::delete_user(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::delete_user(const std::shared_ptr& user, UniqueFunction)>&& completion) { if (!user) { return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found.")); } - if (user->state() != SyncUser::State::LoggedIn) { + if (user->state() != UserState::LoggedIn) { return completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "User must be logged in to be deleted.")); } + util::CheckedLockGuard lock(m_user_mutex); if (!verify_user_present(user)) { return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found.")); } @@ -873,43 +915,47 @@ void App::delete_user(const std::shared_ptr& user, UniqueFunctionidentity()](const Response& response) { - auto error = AppUtils::check_for_errors(response); - if (!error) { - self->emit_change_to_subscribers(*self); - self->m_sync_manager->delete_user(identity); - } - completion(std::move(error)); - }); + do_authenticated_request( + std::move(req), user, + [self = shared_from_this(), completion = std::move(completion), user, this](const Response& response) { + auto error = AppUtils::check_for_errors(response); + if (!error) { + auto user_id = user->user_id(); + user->detach_and_tear_down(); + m_metadata_store->delete_user(*m_file_manager, user_id); + emit_change_to_subscribers(*self); + } + completion(std::move(error)); + }); } -void App::link_user(const std::shared_ptr& user, const AppCredentials& credentials, - UniqueFunction&, Optional)>&& completion) +void App::link_user(const std::shared_ptr& user, const AppCredentials& credentials, + UniqueFunction&, Optional)>&& completion) { if (!user) { return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found.")); } - if (user->state() != SyncUser::State::LoggedIn) { + if (user->state() != UserState::LoggedIn) { return completion(nullptr, AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in.")); } - if (!verify_user_present(user)) { - return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found.")); + if (credentials.provider() == AuthProvider::ANONYMOUS) { + // FIXME: error code + return completion(nullptr, AppError(ErrorCodes::ClientUserNotLoggedIn, + "Cannot add anonymous credentials to an existing user.")); } - App::log_in_with_credentials(credentials, user, std::move(completion)); + log_in_with_credentials(credentials, user, std::move(completion)); } -void App::refresh_custom_data(const std::shared_ptr& user, +void App::refresh_custom_data(const std::shared_ptr& user, UniqueFunction)>&& completion) { refresh_access_token(user, false, std::move(completion)); } -void App::refresh_custom_data(const std::shared_ptr& user, bool update_location, +void App::refresh_custom_data(const std::shared_ptr& user, bool update_location, UniqueFunction)>&& completion) { refresh_access_token(user, update_location, std::move(completion)); @@ -926,9 +972,7 @@ std::string App::get_app_route(const Optional& hostname) const if (hostname) { return util::format("%1%2%3/%4", *hostname, s_base_path, s_app_path, m_config.app_id); } - else { - return m_app_route; - } + return m_app_route; } void App::request_location(UniqueFunction)>&& completion, @@ -993,13 +1037,13 @@ void App::request_location(UniqueFunction)>&& compl "Redirect response missing location header", {}, response.http_status_code}); - return; // early return + return; } // try to request the location info at the new location in the redirect response // retry_count is passed in to track the number of subsequent redirection attempts self->request_location(std::move(completion), std::move(base_url), std::move(redir_location), request.redirect_count); - return; // early return + return; } // Location request was successful - update the location info auto update_response = self->update_location(response, base_url); @@ -1020,21 +1064,15 @@ std::optional App::update_location(const Response& response, const std // a valid response. base_url is the new hostname or m_base_url value when request_location() // was called. - // Check for errors in the response if (auto error = AppUtils::check_for_errors(response)) { return error; } - REALM_ASSERT(m_sync_manager); // Need a valid sync manager - // Update the location info with the data from the response try { auto json = parse(response.body); auto hostname = get(json, "hostname"); auto ws_hostname = get(json, "ws_hostname"); - auto deployment_model = get(json, "deployment_model"); - auto location = get(json, "location"); - log_debug("App: Location info returned for deployment model: %1(%2)", deployment_model, location); { util::CheckedLockGuard guard(m_route_mutex); // Update the local hostname and path information @@ -1146,7 +1184,7 @@ void App::check_for_redirect_response(Request&& request, const Response& respons update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location)); } -void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, +void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, util::UniqueFunction&& completion) { request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken @@ -1167,7 +1205,7 @@ void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, + const std::shared_ptr& sync_user, util::UniqueFunction&& completion) { // Only handle auth failures @@ -1185,25 +1223,23 @@ void App::handle_auth_failure(const AppError& error, const Response& response, R return; } - // Otherwise, refresh the access token - App::refresh_access_token(sync_user, false, - [self = shared_from_this(), request = std::move(request), - completion = std::move(completion), response = std::move(response), - sync_user](Optional&& error) mutable { - if (!error) { - // assign the new access_token to the auth header - request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken); - self->do_request(std::move(request), std::move(completion)); - } - else { - // pass the error back up the chain - completion(std::move(response)); - } - }); + refresh_access_token(sync_user, false, + [self = shared_from_this(), request = std::move(request), completion = std::move(completion), + response = std::move(response), sync_user](Optional&& error) mutable { + if (!error) { + // assign the new access_token to the auth header + request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken); + self->do_request(std::move(request), std::move(completion)); + } + else { + // pass the error back up the chain + completion(std::move(response)); + } + }); } /// MARK: - refresh access token -void App::refresh_access_token(const std::shared_ptr& sync_user, bool update_location, +void App::refresh_access_token(const std::shared_ptr& sync_user, bool update_location, util::UniqueFunction)>&& completion) { if (!sync_user) { @@ -1230,7 +1266,7 @@ void App::refresh_access_token(const std::shared_ptr& sync_user, bool try { auto json = parse(response.body); - sync_user->update_access_token(get(json, "access_token")); + // sync_user->update_access_token(get(json, "access_token")); } catch (AppError& err) { return completion(std::move(err)); @@ -1247,7 +1283,7 @@ std::string App::function_call_url_path() const return util::format("%1/functions/call", m_app_route); } -void App::call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, +void App::call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, const Optional& service_name_opt, UniqueFunction)>&& completion) { @@ -1272,7 +1308,7 @@ void App::call_function(const std::shared_ptr& user, const std::string }); } -void App::call_function(const std::shared_ptr& user, const std::string& name, const BsonArray& args_bson, +void App::call_function(const std::shared_ptr& user, const std::string& name, const BsonArray& args_bson, const Optional& service_name, UniqueFunction&&, Optional)>&& completion) { @@ -1312,7 +1348,7 @@ void App::call_function(const std::shared_ptr& user, const std::string }); } -void App::call_function(const std::shared_ptr& user, const std::string& name, const BsonArray& args_bson, +void App::call_function(const std::shared_ptr& user, const std::string& name, const BsonArray& args_bson, UniqueFunction&&, Optional)>&& completion) { call_function(user, name, args_bson, util::none, std::move(completion)); @@ -1322,16 +1358,16 @@ void App::call_function(const std::string& name, const BsonArray& args_bson, const Optional& service_name, UniqueFunction&&, Optional)>&& completion) { - call_function(m_sync_manager->get_current_user(), name, args_bson, service_name, std::move(completion)); + call_function(current_user(), name, args_bson, service_name, std::move(completion)); } void App::call_function(const std::string& name, const BsonArray& args_bson, UniqueFunction&&, Optional)>&& completion) { - call_function(m_sync_manager->get_current_user(), name, args_bson, std::move(completion)); + call_function(current_user(), name, args_bson, std::move(completion)); } -Request App::make_streaming_request(const std::shared_ptr& user, const std::string& name, +Request App::make_streaming_request(const std::shared_ptr& user, const std::string& name, const BsonArray& args_bson, const Optional& service_name) const { auto args = BsonDocument{ @@ -1362,7 +1398,78 @@ Request App::make_streaming_request(const std::shared_ptr& user, const PushClient App::push_notification_client(const std::string& service_name) { - return PushClient(service_name, m_config.app_id, m_request_timeout_ms, shared_from_this()); + return PushClient(service_name, m_config.app_id, m_request_timeout_ms, + std::shared_ptr(shared_from_this(), this)); +} + +// MARK: - UserProvider + +void App::register_sync_user(SyncUser& sync_user) +{ + REALM_ASSERT(typeid(sync_user) == typeid(User)); + auto& app_user = static_cast(sync_user); + auto& tracked_user = m_users[app_user.user_id()]; + REALM_ASSERT(!tracked_user); + tracked_user = &app_user; + app_user.update_backing_data(m_metadata_store->get_user(app_user.user_id())); +} + +void App::unregister_sync_user(SyncUser& user) +{ + util::CheckedLockGuard lock(m_user_mutex); + REALM_ASSERT(m_users.count(user.user_id())); + m_users.erase(user.user_id()); +} + +void App::request_log_out(std::string_view user_id, CompletionHandler&& completion) +{ + if (auto user = get_existing_logged_in_user(user_id)) { + log_out(user, std::move(completion)); + } +} + +void App::request_refresh_user(std::string_view user_id, CompletionHandler&& completion) +{ + if (auto user = get_existing_logged_in_user(user_id)) { + get_profile(user, [completion = std::move(completion)](auto, auto error) { + completion(std::move(error)); + }); + } +} + +void App::request_refresh_location(std::string_view user_id, CompletionHandler&& completion) +{ + if (auto user = get_existing_logged_in_user(user_id)) { + refresh_access_token(user, true, std::move(completion)); + } +} + +void App::request_access_token(std::string_view user_id, CompletionHandler&& completion) +{ + if (auto user = get_existing_logged_in_user(user_id)) { + refresh_access_token(user, false, std::move(completion)); + } +} + +void App::realm_opened(std::string_view user_id, std::string_view path) +{ + m_metadata_store->add_realm_path(user_id, path); +} + +void App::create_file_action(std::string_view user_id, SyncFileAction action, std::string_view original_path, + std::string_view recovery_path, std::string_view partition_value) +{ + m_metadata_store->create_file_action(action, original_path, recovery_path, partition_value, user_id); +} + +bool App::immediately_run_file_actions(std::string_view realm_path) +{ + return m_metadata_store->immediately_run_file_actions(*m_file_manager, realm_path); +} + +std::string App::path_for_realm(const SyncConfig& config, std::optional custom_file_name) const +{ + return m_file_manager->path_for_realm(config, std::move(custom_file_name)); } } // namespace app diff --git a/src/realm/object-store/sync/app.hpp b/src/realm/object-store/sync/app.hpp index 7d3812884a8..a61a37597a5 100644 --- a/src/realm/object-store/sync/app.hpp +++ b/src/realm/object-store/sync/app.hpp @@ -19,31 +19,34 @@ #ifndef REALM_APP_HPP #define REALM_APP_HPP +#include #include #include #include #include #include #include +#include #include +#include +#include +#include #include #include #include #include -#include - namespace realm { -class SyncUser; class SyncSession; class SyncManager; -struct SyncClientConfig; -class SyncAppMetadata; +class SyncFileManager; namespace app { class App; +class MetadataStore; +class User; typedef std::shared_ptr SharedApp; @@ -53,67 +56,139 @@ typedef std::shared_ptr SharedApp; /// /// You can also use it to execute [Functions](https://docs.mongodb.com/stitch/functions/). class App : public std::enable_shared_from_this, - public AuthRequestClient, - public AppServiceClient, + private AuthRequestClient, + private AppServiceClient, + private UserProvider, public Subscribable { - struct Private {}; public: - struct Config { - // Information about the device where the app is running - struct DeviceInfo { - std::string platform_version; // json: platformVersion - std::string sdk_version; // json: sdkVersion - std::string sdk; // json: sdk - std::string device_name; // json: deviceName - std::string device_version; // json: deviceVersion - std::string framework_name; // json: frameworkName - std::string framework_version; // json: frameworkVersion - std::string bundle_id; // json: bundleId - - DeviceInfo(); - DeviceInfo(std::string, std::string, std::string, std::string, std::string, std::string, std::string, - std::string); - - private: - friend App; - - std::string platform; // json: platform - std::string cpu_arch; // json: cpuArch - std::string core_version; // json: coreVersion - }; - - std::string app_id; - std::shared_ptr transport; - util::Optional base_url; - util::Optional default_request_timeout_ms; - DeviceInfo device_info; + // MARK: - App Initialization + enum class CacheMode { + Enabled, // Return a cached app instance if one was previously generated for `config`'s app_id+base_url combo, + Disabled // Bypass the app cache; return a new app instance. }; + /// Get a shared pointer to a configured App instance. Sync is fully enabled and the external backing store + /// factory provided is used to create a store if the cache is not used. If you want the + /// default storage engine, construct a RealmMetadataStore instance in the factory. + static SharedApp get_app(CacheMode mode, const AppConfig& config); + + /// Return a cached app instance if one was previously generated for the `app_id`+`base_url` combo using + /// `App::get_app()`. + /// If base_url is not provided, and there are multiple cached apps with the same app_id but different base_urls, + /// then a non-determinstic one will be returned. + /// + /// Prefer using `App::get_app()` or populating `base_url` to avoid the non-deterministic behavior. + static SharedApp get_cached_app(const std::string& app_id, + const std::optional& base_url = std::nullopt); + + /// Clear the cache used for `get_app(CacheMode::Enable)` and `get_cached_app()`. + static void clear_cached_apps(); - // `enable_shared_from_this` is unsafe with public constructors; - // use `App::get_app()` instead - explicit App(Private, const Config& config); + explicit App(Private, const AppConfig& config); App(App&&) noexcept = delete; App& operator=(App&&) noexcept = delete; ~App(); - const Config& config() const + const AppConfig& config() const { return m_config; } - /// Get the last used user. - std::shared_ptr current_user() const; + // MARK: - Other objects owned by App + const std::shared_ptr& sync_manager() const + { + return m_sync_manager; + } - /// Get all users. - std::vector> all_users() const; + std::shared_ptr user_provider() + { + return std::shared_ptr(shared_from_this(), this); + } - std::shared_ptr const& sync_manager() const + std::shared_ptr auth_request_client() { - return m_sync_manager; + return std::shared_ptr(shared_from_this(), this); + } + + std::shared_ptr app_service_client() + { + return std::shared_ptr(shared_from_this(), this); } + // MARK: - User Management + + /// Get the last used user. + std::shared_ptr current_user() REQUIRES(!m_user_mutex); + /// Get the user object for the given `user_id` if a user with that id is logged in, or nullptr if not. + std::shared_ptr get_existing_logged_in_user(std::string_view user_id) REQUIRES(!m_user_mutex); + /// Get all logged in users. + std::vector> all_users() REQUIRES(!m_user_mutex); + /// Set the current user to the given one. The user must be logged in and have been obtained from this `App` + /// instance. + void switch_user(const std::shared_ptr& user) REQUIRES(!m_user_mutex); + + /// Log in a user and asynchronously retrieve a user object. + /// If the log in completes successfully, the completion block will be called, and a + /// `User` representing the logged-in user will be passed to it. This user object + /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the + /// completion block will be called with an error. + /// + /// @param credentials A `SyncCredentials` object representing the user to log in. + /// @param completion A callback block to be invoked once the log in completes. + void log_in_with_credentials( + const AppCredentials& credentials, + util::UniqueFunction&, util::Optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Logout the current user. + void log_out(util::UniqueFunction)>&&) REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Refreshes the custom data for a specified user + /// @param user The user you want to refresh + /// @param update_location If true, the location metadata will be updated before refresh + void refresh_custom_data(const std::shared_ptr& user, bool update_location, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + void refresh_custom_data(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + + /// Log out the given user if they are not already logged out. + void log_out(const std::shared_ptr& user, util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + + /// Links the currently authenticated user with a new identity, where the identity is defined by the credential + /// specified as a parameter. This will only be successful if this `User` is the currently authenticated + /// with the client from which it was created. On success the user will be returned with the new identity. + /// + /// @param user The user which will have the credentials linked to, the user must be logged in + /// @param credentials The `AppCredentials` used to link the user to a new identity. + /// @param completion The completion handler to call when the linking is complete. + /// If the operation is successful, the result will contain the original + /// `User` object representing the user. + void link_user(const std::shared_ptr& user, const AppCredentials& credentials, + util::UniqueFunction&, util::Optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + + /// Logs out and removes the provided user. + /// This invokes logout on the server. + /// @param user the user to remove + /// @param completion Will return an error if the user is not found or the http request failed. + void remove_user(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Deletes a user and all its data from the server. + /// @param user The user to delete + /// @param completion Will return an error if the user is not found or the http request failed. + void delete_user(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + // MARK: - Provider Clients + /// A struct representing a user API key as returned by the App server. struct UserAPIKey { // The ID of the key. @@ -138,40 +213,40 @@ class App : public std::enable_shared_from_this, /// Creates a user API key that can be used to authenticate as the current user. /// @param name The name of the API key to be created. /// @param completion A callback to be invoked once the call is complete. - void create_api_key(const std::string& name, const std::shared_ptr& user, + void create_api_key(const std::string& name, const std::shared_ptr& user, util::UniqueFunction)>&& completion); /// Fetches a user API key associated with the current user. /// @param id The id of the API key to fetch. /// @param completion A callback to be invoked once the call is complete. - void fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + void fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, util::UniqueFunction)>&& completion); /// Fetches the user API keys associated with the current user. /// @param completion A callback to be invoked once the call is complete. void - fetch_api_keys(const std::shared_ptr& user, + fetch_api_keys(const std::shared_ptr& user, util::UniqueFunction&&, util::Optional)>&& completion); /// Deletes a user API key associated with the current user. /// @param id The id of the API key to delete. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + void delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, util::UniqueFunction)>&& completion); /// Enables a user API key associated with the current user. /// @param id The id of the API key to enable. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + void enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, util::UniqueFunction)>&& completion); /// Disables a user API key associated with the current user. /// @param id The id of the API key to disable. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + void disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, util::UniqueFunction)>&& completion); private: @@ -251,21 +326,18 @@ class App : public std::enable_shared_from_this, SharedApp m_parent; }; - enum class CacheMode { - Enabled, // Return a cached app instance if one was previously generated for `config`'s app_id+base_url combo, - Disabled // Bypass the app cache; return a new app instance. - }; - /// Get a shared pointer to a configured App instance. - static SharedApp get_app(CacheMode mode, const Config& config, const SyncClientConfig& sync_client_config); - /// Return a cached app instance if one was previously generated for the `app_id`+`base_url` combo using - /// `App::get_app()`. - /// If base_url is not provided, and there are multiple cached apps with the same app_id but different base_urls, - /// then a non-determinstic one will be returned. - /// - /// Prefer using `App::get_app()` or populating `base_url` to avoid the non-deterministic behavior. - static SharedApp get_cached_app(const std::string& app_id, - const std::optional& base_url = std::nullopt); + // Get a provider client for the given class type. + template + T provider_client() + { + return T(this); + } + + // MARK: - App Services + + // Return the base url path used for HTTP AppServices requests + std::string get_host_url() REQUIRES(!m_route_mutex); /// Get the current base URL for the AppServices server used for http requests and sync /// connections. @@ -286,108 +358,35 @@ class App : public std::enable_shared_from_this, void update_base_url(std::optional base_url, util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - /// Log in a user and asynchronously retrieve a user object. - /// If the log in completes successfully, the completion block will be called, and a - /// `SyncUser` representing the logged-in user will be passed to it. This user object - /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the - /// completion block will be called with an error. - /// - /// @param credentials A `SyncCredentials` object representing the user to log in. - /// @param completion A callback block to be invoked once the log in completes. - void log_in_with_credentials( - const AppCredentials& credentials, - util::UniqueFunction&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex); - - /// Logout the current user. - void log_out(util::UniqueFunction)>&&) REQUIRES(!m_route_mutex); - - /// Refreshes the custom data for a specified user - /// @param user The user you want to refresh - /// @param update_location If true, the location metadata will be updated before refresh - void refresh_custom_data(const std::shared_ptr& user, bool update_location, - util::UniqueFunction)>&& completion) - REQUIRES(!m_route_mutex); - void refresh_custom_data(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) - REQUIRES(!m_route_mutex); - - /// Log out the given user if they are not already logged out. - void log_out(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - /// Links the currently authenticated user with a new identity, where the identity is defined by the credential - /// specified as a parameter. This will only be successful if this `SyncUser` is the currently authenticated - /// with the client from which it was created. On success the user will be returned with the new identity. - /// - /// @param user The user which will have the credentials linked to, the user must be logged in - /// @param credentials The `AppCredentials` used to link the user to a new identity. - /// @param completion The completion handler to call when the linking is complete. - /// If the operation is successful, the result will contain the original - /// `SyncUser` object representing the user. - void - link_user(const std::shared_ptr& user, const AppCredentials& credentials, - util::UniqueFunction&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex); - - /// Switches the active user with the specified one. The user must - /// exist in the list of all users who have logged into this application, and - /// the user must be currently logged in, otherwise this will throw an - /// AppError. - /// - /// @param user The user to switch to - /// @returns A shared pointer to the new current user - std::shared_ptr switch_user(const std::shared_ptr& user) const; - /// Logs out and removes the provided user. - /// This invokes logout on the server. - /// @param user the user to remove - /// @param completion Will return an error if the user is not found or the http request failed. - void remove_user(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - /// Deletes a user and all its data from the server. - /// @param user The user to delete - /// @param completion Will return an error if the user is not found or the http request failed. - void delete_user(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - // Get a provider client for the given class type. - template - T provider_client() - { - return T(this); - } - - void call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, + void call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, const util::Optional& service_name, util::UniqueFunction)>&& completion) final REQUIRES(!m_route_mutex); void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name, util::UniqueFunction&&, util::Optional)>&& completion) final REQUIRES(!m_route_mutex); void call_function( - const std::shared_ptr& user, const std::string&, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string&, const bson::BsonArray& args_bson, util::UniqueFunction&&, util::Optional)>&& completion) final REQUIRES(!m_route_mutex); void call_function( const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name, util::UniqueFunction&&, util::Optional)>&& completion) final - REQUIRES(!m_route_mutex); + REQUIRES(!m_route_mutex, !m_user_mutex); void call_function( const std::string&, const bson::BsonArray& args_bson, util::UniqueFunction&&, util::Optional)>&& completion) final - REQUIRES(!m_route_mutex); + REQUIRES(!m_route_mutex, !m_user_mutex); template - void call_function(const std::shared_ptr& user, const std::string& name, - const bson::BsonArray& args_bson, + void call_function(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, util::UniqueFunction&&, util::Optional)>&& completion) REQUIRES(!m_route_mutex) { @@ -413,33 +412,36 @@ class App : public std::enable_shared_from_this, // NOTE: only sets "Accept: text/event-stream" header. If you use an API that sets that but doesn't support // setting other headers (eg. EventSource() in JS), you can ignore the headers field on the request. - Request make_streaming_request(const std::shared_ptr& user, const std::string& name, + Request make_streaming_request(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name) const REQUIRES(!m_route_mutex); - // MARK: Push notification client PushClient push_notification_client(const std::string& service_name); - static void clear_cached_apps(); + // MARK: - Sync // Immediately close all open sync sessions for all cached apps. // Used by JS SDK to ensure no sync clients remain open when a developer // reloads an app (#5411). static void close_all_sync_sessions(); - // Return the base url path used for HTTP AppServices requests - std::string get_host_url() REQUIRES(!m_route_mutex); - // Return the base url path used for Sync Session Websocket requests std::string get_ws_host_url() REQUIRES(!m_route_mutex); -private: - // Local copy of app config - Config m_config; + // Get the default path for a Realm for the given configuration. + // The default value is `///.realm`. + // If the file cannot be created at this location, for example due to path length restrictions, + // this function may pass back `/.realm` + std::string path_for_realm(const SyncConfig& config, std::optional custom_file_name) const; - // mutable to allow locking for reads in const functions - mutable util::CheckedMutex m_route_mutex; + // Attempt to perform all pending file actions for the given path. Returns + // true if any were performed. + bool immediately_run_file_actions(std::string_view realm_path); + +private: + const AppConfig m_config; + util::CheckedMutex m_route_mutex; // The following variables hold the different paths to Atlas, depending on the // request being performed // Base hostname from config.base_url or update_base_url() for querying location info @@ -464,7 +466,9 @@ class App : public std::enable_shared_from_this, // Base hostname for Device Sync websocket requests std::string m_ws_host_url GUARDED_BY(m_route_mutex); - uint64_t m_request_timeout_ms; + const uint64_t m_request_timeout_ms; + std::unique_ptr m_file_manager; + std::unique_ptr m_metadata_store; std::shared_ptr m_sync_manager; std::shared_ptr m_logger_ptr; @@ -480,10 +484,10 @@ class App : public std::enable_shared_from_this, template void log_error(const char* message, Params&&... params); - /// Refreshes the access token for a specified `SyncUser` + /// Refreshes the access token for a specified `User` /// @param completion Passes an error should one occur. /// @param update_location If true, the location metadata will be updated before refresh - void refresh_access_token(const std::shared_ptr& user, bool update_location, + void refresh_access_token(const std::shared_ptr& user, bool update_location, util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); @@ -495,7 +499,7 @@ class App : public std::enable_shared_from_this, /// @param completion returns the original response in the case it is not an auth error, or if a failure /// occurs, if the refresh was a success the newly attempted response will be passed back void handle_auth_failure(const AppError& error, const Response& response, Request&& request, - const std::shared_ptr& sync_user, + const std::shared_ptr& user, util::UniqueFunction&& completion) REQUIRES(!m_route_mutex); std::string url_for_path(const std::string& path) const override REQUIRES(!m_route_mutex); @@ -551,53 +555,81 @@ class App : public std::enable_shared_from_this, /// Performs an authenticated request to the Stitch server, using the current authentication state /// @param request The request to be performed /// @param completion Returns the response from the server - void do_authenticated_request(Request&& request, const std::shared_ptr& user, + void do_authenticated_request(Request&& request, const std::shared_ptr& user, util::UniqueFunction&& completion) override REQUIRES(!m_route_mutex); - /// Gets the social profile for a `SyncUser` - /// @param completion Callback will pass the `SyncUser` with the social profile details - void - get_profile(const std::shared_ptr& user, - util::UniqueFunction&, util::Optional)>&& completion) + /// Gets the social profile for a `User`. + /// + /// @param completion Callback will pass the `User` with the social profile details + void get_profile(const std::shared_ptr& user, + util::UniqueFunction&, util::Optional)>&& completion) REQUIRES(!m_route_mutex); /// Log in a user and asynchronously retrieve a user object. /// If the log in completes successfully, the completion block will be called, and a - /// `SyncUser` representing the logged-in user will be passed to it. This user object + /// `User` representing the logged-in user will be passed to it. This user object /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the /// completion block will be called with an error. /// /// @param credentials A `SyncCredentials` object representing the user to log in. - /// @param linking_user A `SyncUser` you want to link these credentials too + /// @param linking_user A `User` you want to link these credentials too /// @param completion A callback block to be invoked once the log in completes. void log_in_with_credentials( - const AppCredentials& credentials, const std::shared_ptr& linking_user, - util::UniqueFunction&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex); + const AppCredentials& credentials, const std::shared_ptr& linking_user, + util::UniqueFunction&, util::Optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); /// Provides MongoDB Realm Cloud with metadata related to the users session void attach_auth_options(bson::BsonDocument& body); std::string function_call_url_path() const REQUIRES(!m_route_mutex); - void configure(const SyncClientConfig& sync_client_config) REQUIRES(!m_route_mutex); + static SharedApp do_get_app(CacheMode mode, const AppConfig& config, + util::FunctionRef do_config); + + void configure_backing_store(std::unique_ptr store) REQUIRES(!m_route_mutex); - // Requires locking m_route_mutex before calling std::string make_sync_route(util::Optional ws_host_url = util::none) REQUIRES(m_route_mutex); - // Requires locking m_route_mutex before calling - void configure_route(const std::string& host_url, const std::optional& ws_host_url = std::nullopt) - REQUIRES(m_route_mutex); + void configure_route(const std::string& host_url, const std::string& ws_host_url = "") REQUIRES(m_route_mutex); - // Requires locking m_route_mutex before calling void update_hostname(const std::string& host_url, const std::optional& ws_host_url = std::nullopt, const std::optional& new_base_url = std::nullopt) REQUIRES(m_route_mutex); std::string auth_route() REQUIRES(!m_route_mutex); std::string base_url() REQUIRES(!m_route_mutex); - bool verify_user_present(const std::shared_ptr& user) const; + bool verify_user_present(const std::shared_ptr& user) const REQUIRES(m_user_mutex); + + // UserProvider implementation + friend class User; + + util::CheckedMutex m_user_mutex; + mutable std::unordered_map m_users GUARDED_BY(m_user_mutex); + User* m_current_user GUARDED_BY(m_user_mutex) = nullptr; + + void register_sync_user(SyncUser& sync_user) override REQUIRES(m_user_mutex); + void unregister_sync_user(SyncUser& user) override REQUIRES(!m_user_mutex); + void request_log_out(std::string_view user_id, CompletionHandler&&) override + REQUIRES(!m_user_mutex, !m_route_mutex); + void request_refresh_user(std::string_view user_id, CompletionHandler&&) override + REQUIRES(!m_user_mutex, !m_route_mutex); + void request_refresh_location(std::string_view user_id, CompletionHandler&&) override + REQUIRES(!m_user_mutex, !m_route_mutex); + void request_access_token(std::string_view user_id, CompletionHandler&&) override + REQUIRES(!m_user_mutex, !m_route_mutex); + void realm_opened(std::string_view user_id, std::string_view path) override REQUIRES(!m_user_mutex); + void create_file_action(std::string_view user_id, SyncFileAction action, std::string_view original_path, + std::string_view recovery_path, std::string_view partition_value) override + REQUIRES(!m_user_mutex); + + // user helpers + std::shared_ptr get_user_for_id(const std::string& user_id) REQUIRES(m_user_mutex); + void user_data_updated(const std::string& user_id) REQUIRES(m_user_mutex); + void do_remove_user(SyncUser& user); + void log_out(const std::shared_ptr& user, UserState new_state, + util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); }; // MARK: Provider client templates diff --git a/src/realm/object-store/sync/app_config.hpp b/src/realm/object-store/sync/app_config.hpp new file mode 100644 index 00000000000..507cb39ec0f --- /dev/null +++ b/src/realm/object-store/sync/app_config.hpp @@ -0,0 +1,105 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or utilied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_SYNC_APP_CONFIG_HPP +#define REALM_OS_SYNC_APP_CONFIG_HPP + +#include +#include +#include +#include +#include + +namespace realm { +struct SyncClientTimeouts { + SyncClientTimeouts(); + // See sync::Client::Config for the meaning of these fields. + uint64_t connect_timeout; + uint64_t connection_linger_time; + uint64_t ping_keepalive_period; + uint64_t pong_keepalive_timeout; + uint64_t fast_reconnect_limit; +}; + +struct SyncClientConfig { + using LoggerFactory = std::function(util::Logger::Level)>; + LoggerFactory logger_factory; + util::Logger::Level log_level = util::Logger::Level::info; + ReconnectMode reconnect_mode = ReconnectMode::normal; // For internal sync-client testing only! +#if REALM_DISABLE_SYNC_MULTIPLEXING + bool multiplex_sessions = false; +#else + bool multiplex_sessions = true; +#endif + + // The SyncSocket instance used by the Sync Client for event synchronization + // and creating WebSockets. If not provided the default implementation will be used. + std::shared_ptr socket_provider; + + // Optional thread observer for event loop thread events in the default SyncSocketProvider + // implementation. It is not used for custom SyncSocketProvider implementations. + std::shared_ptr default_socket_provider_thread_observer; + + // {@ + // Optional information about the binding/application that is sent as part of the User-Agent + // when establishing a connection to the server. These values are only used by the default + // SyncSocket implementation. Custom SyncSocket implementations must update the User-Agent + // directly, if supported by the platform APIs. + std::string user_agent_binding_info; + std::string user_agent_application_info; + // @} + + SyncClientTimeouts timeouts; +}; + +namespace app { +struct AppConfig { + // Information about the device where the app is running + struct DeviceInfo { + std::string platform_version; // json: platformVersion + std::string sdk_version; // json: sdkVersion + std::string sdk; // json: sdk + std::string device_name; // json: deviceName + std::string device_version; // json: deviceVersion + std::string framework_name; // json: frameworkName + std::string framework_version; // json: frameworkVersion + std::string bundle_id; // json: bundleId + }; + + std::string app_id; + std::shared_ptr transport; + std::optional base_url; + std::optional default_request_timeout_ms; + DeviceInfo device_info; + + std::string base_file_path; + SyncClientConfig sync_client_config; + + enum class MetadataMode { + NoEncryption, // Enable metadata, but disable encryption. + Encryption, // Enable metadata, and use encryption (automatic if possible). + InMemory, // Do not persist metadata + }; + MetadataMode metadata_mode = MetadataMode::Encryption; + std::optional> custom_encryption_key; +}; + +} // namespace app +} // namespace realm + +#endif // REALM_OS_SYNC_APP_CONFIG_HPP diff --git a/src/realm/object-store/sync/app_service_client.hpp b/src/realm/object-store/sync/app_service_client.hpp index 9bd913432c4..922e9286435 100644 --- a/src/realm/object-store/sync/app_service_client.hpp +++ b/src/realm/object-store/sync/app_service_client.hpp @@ -26,8 +26,8 @@ #include namespace realm { -class SyncUser; namespace app { +class User; struct AppError; /// A class providing the core functionality necessary to make authenticated @@ -46,7 +46,7 @@ class AppServiceClient { /// the case of error. Using a string* rather than optional to avoid copying a potentially large /// string. virtual void - call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, + call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, const util::Optional& service_name, util::UniqueFunction)>&& completion) = 0; @@ -58,7 +58,7 @@ class AppServiceClient { /// @param completion Returns the result from the intended call, will return an Optional AppError is an /// error is thrown and bson if successful virtual void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name, util::UniqueFunction&&, util::Optional)>&& completion) = 0; @@ -69,7 +69,7 @@ class AppServiceClient { /// @param completion Returns the result from the intended call, will return an Optional AppError is an /// error is thrown and bson if successful virtual void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, util::UniqueFunction&&, util::Optional)>&& completion) = 0; /// Calls the Realm Cloud function with the provided name and arguments. diff --git a/src/realm/object-store/sync/app_user.cpp b/src/realm/object-store/sync/app_user.cpp new file mode 100644 index 00000000000..97fcf35c29f --- /dev/null +++ b/src/realm/object-store/sync/app_user.cpp @@ -0,0 +1,145 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include + +namespace realm::app { + +UserIdentity::UserIdentity(const std::string& id, const std::string& provider_type) + : id(id) + , provider_type(provider_type) +{ +} + +User::User(Private, std::shared_ptr app, std::string_view user_id) + : SyncUser(app->user_provider(), app->sync_manager(), app->config().app_id, user_id) +{ +} + +void User::update_backing_data(std::pair&& data) +{ + { + util::CheckedLockGuard lock(m_mutex); + m_app_data = std::move(data.second); + } + SyncUser::update_backing_data(std::move(data.first)); + emit_change_to_subscribers(*this); +} + +std::vector User::identities() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_app_data.identities; +} + +void User::log_out() +{ + m_provider->request_log_out(m_user_id, nullptr); +} + +bool User::is_anonymous() const +{ + util::CheckedLockGuard lock(m_mutex); + return do_is_anonymous(); +} + +bool User::do_is_anonymous() const +{ + return m_data.state == UserState::LoggedIn && m_app_data.identities.size() == 1 && + m_app_data.identities[0].provider_type == app::IdentityProviderAnonymous; +} + +std::string User::device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_app_data.device_id; +} + +bool User::has_device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return !m_app_data.device_id.empty() && m_app_data.device_id != "000000000000000000000000"; +} + +UserProfile User::user_profile() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_app_data.profile; +} + +util::Optional User::custom_data() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.access_token.user_data; +} + +std::shared_ptr User::app() const +{ + util::CheckedLockGuard lock(m_mutex); + // std::shared_pointer_cast, but from a private base class + return std::shared_ptr(m_provider, static_cast(m_provider.get())); +} + +app::MongoClient User::mongo_client(const std::string& service_name) +{ + util::CheckedLockGuard lock(m_mutex); + return app::MongoClient(shared_from_this(), static_cast(m_provider.get())->app_service_client(), + service_name); +} + +void User::refresh_custom_data(util::UniqueFunction)> completion_block) + REQUIRES(!m_mutex) +{ + refresh_custom_data(false, std::move(completion_block)); +} + +void User::refresh_custom_data(bool update_location, + util::UniqueFunction)> completion_block) +{ + std::shared_ptr app; + std::shared_ptr user; + { + util::CheckedLockGuard lk(m_mutex); + if (m_data.state != UserState::Removed) { + user = shared_from_this(); + } + } + if (!user) { + completion_block(app::AppError( + ErrorCodes::ClientUserNotFound, + util::format("Cannot initiate a refresh on user '%1' because the user has been removed", m_user_id))); + } + else { + app->refresh_custom_data(user, update_location, std::move(completion_block)); + } +} +} // namespace realm::app + +namespace std { +size_t hash::operator()(const realm::app::UserIdentity& k) const +{ + return ((hash()(k.id) ^ (hash()(k.provider_type) << 1)) >> 1); +} +} // namespace std diff --git a/src/realm/object-store/sync/app_user.hpp b/src/realm/object-store/sync/app_user.hpp new file mode 100644 index 00000000000..3d615f8ff3f --- /dev/null +++ b/src/realm/object-store/sync/app_user.hpp @@ -0,0 +1,209 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_APP_USER_HPP +#define REALM_OS_APP_USER_HPP + +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace realm::app { +class App; +struct AppError; +class MetadataStore; +class MongoClient; + +struct UserProfile { + // The full name of the user. + util::Optional name() const + { + return get_field("name"); + } + // The email address of the user. + util::Optional email() const + { + return get_field("email"); + } + // A URL to the user's profile picture. + util::Optional picture_url() const + { + return get_field("picture_url"); + } + // The first name of the user. + util::Optional first_name() const + { + return get_field("first_name"); + } + // The last name of the user. + util::Optional last_name() const + { + return get_field("last_name"); + } + // The gender of the user. + util::Optional gender() const + { + return get_field("gender"); + } + // The birthdate of the user. + util::Optional birthday() const + { + return get_field("birthday"); + } + // The minimum age of the user. + util::Optional min_age() const + { + return get_field("min_age"); + } + // The maximum age of the user. + util::Optional max_age() const + { + return get_field("max_age"); + } + + bson::Bson operator[](const std::string& key) const + { + return m_data.at(key); + } + + const bson::BsonDocument& data() const + { + return m_data; + } + + UserProfile(bson::BsonDocument&& data) + : m_data(std::move(data)) + { + } + UserProfile() = default; + +private: + bson::BsonDocument m_data; + + util::Optional get_field(const char* name) const + { + auto it = m_data.find(name); + if (it == m_data.end()) { + return util::none; + } + return static_cast((*it).second); + } +}; + +// A struct that represents an identity that a `User` is linked to +struct UserIdentity { + // the id of the identity + std::string id; + // the associated provider type of the identity + std::string provider_type; + + UserIdentity(const std::string& id, const std::string& provider_type); + + bool operator==(const UserIdentity& other) const + { + return id == other.id && provider_type == other.provider_type; + } + + bool operator!=(const UserIdentity& other) const + { + return id != other.id || provider_type != other.provider_type; + } +}; + +struct UserData { + // Identities which were used to log into this user + std::vector identities; + // Id for the device which this user was logged in on. Users are not + // portable between devices so this cannot be changed after the user + // is created + std::string device_id; + // Server-stored user profile + UserProfile profile; +}; + +class User final : public SyncUser, public std::enable_shared_from_this, public Subscribable { + struct Private {}; + using SyncUser::m_mutex; + +public: + // Log the user out and mark it as such. This will also close its associated Sessions. + void log_out() REQUIRES(!m_mutex); + + /// Returns true if the user's only identity is anonymous. + bool is_anonymous() const REQUIRES(!m_mutex); + + std::string device_id() const REQUIRES(!m_mutex); + bool has_device_id() const REQUIRES(!m_mutex); + UserProfile user_profile() const REQUIRES(!m_mutex); + std::vector identities() const REQUIRES(!m_mutex); + + // Custom user data embedded in the access token. + util::Optional custom_data() const REQUIRES(!m_mutex); + + // Get the app instance that this user belongs to. + std::shared_ptr app() const REQUIRES(!m_mutex); + + /// Retrieves a general-purpose service client for the Realm Cloud service + /// @param service_name The name of the cluster + app::MongoClient mongo_client(const std::string& service_name) REQUIRES(!m_mutex); + + // ------------------------------------------------------------------------ + // All of the following are called by `RealmMetadataStore` and are public only for + // testing purposes. SDKs should not call these directly in non-test code + // or expose them in the public API. + + static std::shared_ptr make(std::shared_ptr app, std::string_view user_id) + { + return std::make_shared(Private(), std::move(app), user_id); + } + + User(Private, std::shared_ptr app, std::string_view user_id); + + // Atomically set the user to be logged in and update both tokens. + void update_backing_data(std::pair&& data) REQUIRES(!m_mutex); + + /// Refreshes the custom data for this user + /// If `update_location` is true, the location metadata will be queried before the request + void refresh_custom_data(bool update_location, + util::UniqueFunction)> completion_block) + REQUIRES(!m_mutex); + void refresh_custom_data(util::UniqueFunction)> completion_block) + REQUIRES(!m_mutex); + +private: + bool do_is_anonymous() const REQUIRES(m_mutex); + UserData m_app_data GUARDED_BY(m_mutex); +}; + +} // namespace realm::app + +namespace std { +template <> +struct hash { + size_t operator()(realm::app::UserIdentity const&) const; +}; +} // namespace std + +#endif // REALM_OS_SYNC_USER_HPP diff --git a/src/realm/object-store/sync/app_utils.cpp b/src/realm/object-store/sync/app_utils.cpp index 9435a5959d2..d04760eac93 100644 --- a/src/realm/object-store/sync/app_utils.cpp +++ b/src/realm/object-store/sync/app_utils.cpp @@ -45,7 +45,7 @@ AppUtils::find_header(const std::string& key_name, const std::map AppUtils::split_url(std::string url) +StatusWith AppUtils::split_url(std::string_view url) { UrlComponents comp; // Find the position of the scheme separator "://" @@ -55,7 +55,7 @@ StatusWith AppUtils::split_url(std::string url) return {ErrorCodes::BadServerUrl, util::format("URL missing scheme separator '://': %1", url)}; } comp.scheme = url.substr(0, scheme_end_pos); - url.erase(0, scheme_end_pos + std::char_traits::length("://")); + url = url.substr(scheme_end_pos + 3); // Find the first slash "/" size_t host_end_pos = url.find("/"); diff --git a/src/realm/object-store/sync/app_utils.hpp b/src/realm/object-store/sync/app_utils.hpp index eb121c698df..72b423a84e8 100644 --- a/src/realm/object-store/sync/app_utils.hpp +++ b/src/realm/object-store/sync/app_utils.hpp @@ -38,7 +38,7 @@ class AppUtils { }; // Split the URL into scheme, server and request parts // returns nullopt if missing `://` or server info is empty - static StatusWith split_url(std::string url); + static StatusWith split_url(std::string_view url); static std::optional check_for_errors(const Response& response); static Response make_apperror_response(const AppError& error); diff --git a/src/realm/object-store/sync/auth_request_client.hpp b/src/realm/object-store/sync/auth_request_client.hpp index 824246af9bd..8f0bc763adb 100644 --- a/src/realm/object-store/sync/auth_request_client.hpp +++ b/src/realm/object-store/sync/auth_request_client.hpp @@ -24,8 +24,8 @@ #include namespace realm { -class SyncUser; namespace app { +class User; struct Request; struct Response; @@ -35,7 +35,7 @@ class AuthRequestClient { virtual std::string url_for_path(const std::string& path) const = 0; - virtual void do_authenticated_request(Request&&, const std::shared_ptr& sync_user, + virtual void do_authenticated_request(Request&&, const std::shared_ptr& sync_user, util::UniqueFunction&&) = 0; }; diff --git a/src/realm/object-store/sync/impl/app_metadata.cpp b/src/realm/object-store/sync/impl/app_metadata.cpp new file mode 100644 index 00000000000..e30a3faee5d --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.cpp @@ -0,0 +1,827 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include +#if REALM_PLATFORM_APPLE +#include +#endif + +#include +#include +#include + +using namespace realm; + +namespace { + +struct CurrentUserSchema { + TableKey table_key; + ColKey user_id; + + static constexpr const char* table_name = "current_user_identity"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id = object_schema->persisted_properties[0].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, {{table_name, PropertyType::String}}}; + } +}; + +struct UserIdentitySchema { + TableKey table_key; + ColKey user_id; + ColKey provider_id; + + static constexpr const char* table_name = "UserIdentity"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id = object_schema->persisted_properties[0].column_key; + provider_id = object_schema->persisted_properties[1].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + ObjectSchema::ObjectType::Embedded, + { + {"id", PropertyType::String}, + {"provider_type", PropertyType::String}, + }}; + } +}; + +struct SyncUserSchema { + TableKey table_key; + + // The server-supplied user_id for the user. Unique per server instance. + ColKey user_id_col; + // Locally generated UUIDs for the user. These are tracked to be able + // to open pre-existing Realm files, but are no longer generated or + // used for anything else. + ColKey legacy_uuids_col; + // The cached refresh token for this user. + ColKey refresh_token_col; + // The cached access token for this user. + ColKey access_token_col; + // The identities for this user. + ColKey identities_col; + // The current state of this user. + ColKey state_col; + // The device id of this user. + ColKey device_id_col; + // Any additional profile attributes, formatted as a bson string. + ColKey profile_dump_col; + // The set of absolute file paths to Realms belonging to this user. + ColKey realm_file_paths_col; + + static constexpr const char* table_name = "UserMetadata"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id_col = object_schema->persisted_properties[0].column_key; + legacy_uuids_col = object_schema->persisted_properties[1].column_key; + refresh_token_col = object_schema->persisted_properties[2].column_key; + access_token_col = object_schema->persisted_properties[3].column_key; + identities_col = object_schema->persisted_properties[4].column_key; + state_col = object_schema->persisted_properties[5].column_key; + device_id_col = object_schema->persisted_properties[6].column_key; + profile_dump_col = object_schema->persisted_properties[7].column_key; + realm_file_paths_col = object_schema->persisted_properties[8].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + {{"identity", PropertyType::String}, + {"legacy_uuids", PropertyType::String | PropertyType::Array}, + {"refresh_token", PropertyType::String | PropertyType::Nullable}, + {"access_token", PropertyType::String | PropertyType::Nullable}, + {"identities", PropertyType::Object | PropertyType::Array, UserIdentitySchema::table_name}, + {"state", PropertyType::Int}, + {"device_id", PropertyType::String}, + {"profile_data", PropertyType::String}, + {"local_realm_paths", PropertyType::Set | PropertyType::String}}}; + } +}; + +struct FileActionSchema { + TableKey table_key; + + // The original path on disk of the file (generally, the main file for an on-disk Realm). + ColKey idx_original_name; + // A new path on disk for a file to be written to. Context-dependent. + ColKey idx_new_name; + // An enum describing the action to take. + ColKey idx_action; + // The partition key of the Realm. + ColKey idx_partition; + // The user_id of the user to whom the file action applies (despite the internal column name). + ColKey idx_user_identity; + + static constexpr const char* table_name = "FileActionMetadata"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + idx_original_name = object_schema->persisted_properties[0].column_key; + idx_new_name = object_schema->persisted_properties[1].column_key; + idx_action = object_schema->persisted_properties[2].column_key; + idx_partition = object_schema->persisted_properties[3].column_key; + idx_user_identity = object_schema->persisted_properties[4].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + { + {"original_name", PropertyType::String, Property::IsPrimary{true}}, + {"new_name", PropertyType::String | PropertyType::Nullable}, + {"action", PropertyType::Int}, + {"url", PropertyType::String}, // actually partition key + {"identity", PropertyType::String}, // actually user id + }}; + } +}; + +void migrate_to_v7(std::shared_ptr old_realm, std::shared_ptr realm) +{ + // Before schema version 7 there may have been multiple UserMetadata entries + // for a single user_id with different provider types, so we need to merge + // any duplicates together + + SyncUserSchema schema; + schema.read(*realm); + + TableRef table = realm->read_group().get_table(schema.table_key); + TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), SyncUserSchema::table_name); + if (table->is_empty()) + return; + REALM_ASSERT(table->size() == old_table->size()); + + ColKey old_uuid_col = old_table->get_column_key("local_uuid"); + + std::unordered_map users; + for (size_t i = 0, j = 0; i < table->size(); ++j) { + auto obj = table->get_object(i); + + // Move the local uuid from the old column to the list + auto old_obj = old_table->get_object(j); + obj.get_list(schema.legacy_uuids_col).add(old_obj.get(old_uuid_col)); + + // Check if we've already seen an object with the same id. If not, store + // this one and move on + std::string user_id = obj.get(schema.user_id_col); + auto& existing = users[obj.get(schema.user_id_col)]; + if (!existing.is_valid()) { + existing = obj; + ++i; + continue; + } + + // We have a second object for the same id, so we need to merge them. + // First we merge the state: if one is logged in and the other isn't, + // we'll use the logged-in state and tokens. If both are logged in, we'll + // use the more recent login. If one is logged out and the other is + // removed we'll use the logged out state. If both are logged out or + // both are removed then it doesn't matter which we pick. + using State = UserState; + auto state = State(obj.get(schema.state_col)); + auto existing_state = State(existing.get(schema.state_col)); + if (state == existing_state) { + if (state == State::LoggedIn) { + RealmJWT token_1(existing.get(schema.access_token_col)); + RealmJWT token_2(obj.get(schema.access_token_col)); + if (token_1.issued_at < token_2.issued_at) { + existing.set(schema.refresh_token_col, obj.get(schema.refresh_token_col)); + existing.set(schema.access_token_col, obj.get(schema.access_token_col)); + } + } + } + else if (state == State::LoggedIn || existing_state == State::Removed) { + existing.set(schema.state_col, int64_t(state)); + existing.set(schema.refresh_token_col, obj.get(schema.refresh_token_col)); + existing.set(schema.access_token_col, obj.get(schema.access_token_col)); + } + + // Next we merge the list properties (identities, legacy uuids, realm file paths) + { + auto dest = existing.get_linklist(schema.identities_col); + auto src = obj.get_linklist(schema.identities_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_list(schema.legacy_uuids_col); + auto src = obj.get_list(schema.legacy_uuids_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_set(schema.realm_file_paths_col); + auto src = obj.get_set(schema.realm_file_paths_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + dest.insert(src.get(i)); + } + } + + // Finally we delete the duplicate object. We don't increment `i` as it's + // now the index of the object just after the one we're deleting. + obj.remove(); + } +} + +std::shared_ptr try_get_realm(const RealmConfig& config) +{ + try { + return Realm::get_shared_realm(config); + } + catch (const InvalidDatabase&) { + return nullptr; + } + // catch (const InvalidSchemaVersionException&) { + // return nullptr; + // } +} + +std::shared_ptr open_realm(RealmConfig& config, bool should_encrypt, bool caller_supplied_key) +{ + if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { + if (auto realm = try_get_realm(config)) + return realm; + + // Encryption key changed, so delete the existing metadata realm and + // recreate it + util::File::remove(config.path); + return try_get_realm(config); + } + +#if REALM_PLATFORM_APPLE + // This logic is all a giant race condition once we have multi-process sync. + // Wrapping it all (including the keychain accesses) in DB::call_with_lock() + // might suffice. + + // First try to open the Realm with a key already stored in the keychain. + // This works for both the case where everything is sensible and valid and + // when we have a key but no metadata Realm. + auto key = keychain::get_existing_metadata_realm_key(); + if (key) { + config.encryption_key = *key; + if (auto realm = try_get_realm(config)) + return realm; + } + + // If we have an existing file and either no key or the key didn't work to + // decrypt it, then we might have an unencrypted metadata Realm resulting + // from a previous run being unable to access the keychain. + if (util::File::exists(config.path)) { + config.encryption_key.clear(); + if (auto realm = try_get_realm(config)) + return realm; + + // We weren't able to open the existing file with either the stored key + // or no key, so just delete it. + util::File::remove(config.path); + } + + // We now have no metadata Realm. If we don't have an existing stored key, + // try to create and store a new one. This might fail, in which case we + // just create an unencrypted Realm file. + if (!key) + key = keychain::create_new_metadata_realm_key(); + if (key) + config.encryption_key = std::move(*key); + return try_get_realm(config); +#else // REALM_PLATFORM_APPLE + REALM_UNREACHABLE(); +#endif // REALM_PLATFORM_APPLE +} + +struct PersistedSyncMetadataManager : public app::MetadataStore { + RealmConfig m_config; + SyncUserSchema m_user_schema; + FileActionSchema m_file_action_schema; + UserIdentitySchema m_user_identity_schema; + CurrentUserSchema m_current_user_schema; + + PersistedSyncMetadataManager(std::string path, bool should_encrypt, + util::Optional> encryption_key, SyncFileManager& file_manager) + { + constexpr uint64_t SCHEMA_VERSION = 7; + + if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) + throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); + + m_config.automatic_change_notifications = false; + m_config.path = path; + m_config.schema = Schema{ + UserIdentitySchema::object_schema(), + SyncUserSchema::object_schema(), + FileActionSchema::object_schema(), + CurrentUserSchema::object_schema(), + }; + + m_config.schema_version = SCHEMA_VERSION; + m_config.schema_mode = SchemaMode::Automatic; + m_config.scheduler = util::Scheduler::make_dummy(); + if (encryption_key) + m_config.encryption_key = std::move(*encryption_key); + m_config.automatically_handle_backlinks_in_migrations = true; + m_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, Schema&) { + if (old_realm->schema_version() < 7) { + migrate_to_v7(old_realm, realm); + } + }; + + auto realm = open_realm(m_config, should_encrypt, encryption_key != none); + m_user_schema.read(*realm); + m_file_action_schema.read(*realm); + m_current_user_schema.read(*realm); + + realm->begin_transaction(); + perform_file_actions(*realm, file_manager); + remove_dead_users(*realm, file_manager); + realm->commit_transaction(); + } + + std::shared_ptr get_realm() const + { + return Realm::get_shared_realm(m_config); + } + + void remove_dead_users(Realm& realm, SyncFileManager& file_manager) + { + auto& schema = m_user_schema; + TableRef table = realm.read_group().get_table(schema.table_key); + for (auto it = table->begin(); it != table->end();) { + auto obj = *it; + if (static_cast(obj.get(schema.state_col)) != UserState::Removed) { + ++it; + continue; + } + delete_user_realms(file_manager, obj); + } + } + + void delete_user_realms(SyncFileManager& file_manager, Obj& obj) + { + Set paths = obj.get_set(m_user_schema.realm_file_paths_col); + for (auto path : paths) { + file_manager.remove_realm(path); + } + try { + file_manager.remove_user_realms(obj.get(m_user_schema.user_id_col), {}); + } + catch (FileAccessError const&) { + // Files that we've tracked may no longer exist and that's fine + } + obj.remove(); + } + + bool perform_file_action(SyncFileManager& file_manager, Obj& obj) + { + auto& schema = m_file_action_schema; + switch (static_cast(obj.get(schema.idx_action))) { + case SyncFileAction::DeleteRealm: + // Delete all the files for the given Realm. + return file_manager.remove_realm(obj.get(schema.idx_original_name)); + + case SyncFileAction::BackUpThenDeleteRealm: + // Copy the primary Realm file to the recovery dir, and then delete the Realm. + auto new_name = obj.get(schema.idx_new_name); + auto original_name = obj.get(schema.idx_original_name); + if (!util::File::exists(original_name)) { + // The Realm file doesn't exist anymore, which is fine + return true; + } + + if (new_name && !util::File::exists(new_name) && + file_manager.copy_realm_file(original_name, new_name)) { + // We successfully copied the Realm file to the recovery directory. + bool did_remove = file_manager.remove_realm(original_name); + // if the copy succeeded but not the delete, then running BackupThenDelete + // a second time would fail, so change this action to just delete the original file. + if (did_remove) { + return true; + } + obj.set(schema.idx_action, static_cast(SyncFileAction::DeleteRealm)); + } + return false; + } + } + + void perform_file_actions(Realm& realm, SyncFileManager& file_manager) + { + TableRef table = realm.read_group().get_table(m_file_action_schema.table_key); + if (table->is_empty()) + return; + + for (auto it = table->begin(); it != table->end();) { + auto obj = *it; + if (perform_file_action(file_manager, obj)) + obj.remove(); + else + ++it; + } + } + + bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view realm_path) override + { + auto realm = get_realm(); + realm->begin_transaction(); + TableRef table = realm->read_group().get_table(m_file_action_schema.table_key); + auto key = table->where().equal(m_file_action_schema.idx_original_name, StringData(realm_path)).find(); + if (!key) { + return false; + } + auto obj = table->get_object(key); + bool did_run = perform_file_action(file_manager, obj); + if (did_run) + obj.remove(); + realm->commit_transaction(); + return did_run; + } + + bool has_user(std::string_view user_id) override + { + auto realm = get_realm(); + auto obj = find_user(*realm, user_id); + return is_valid_user(obj); + } + + Data get_user(std::string_view user_id) override + { + auto realm = get_realm(); + return read_user(find_user(*realm, user_id)); + } + + void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token, + std::string_view device_id) override + { + auto realm = get_realm(); + realm->begin_transaction(); + + auto& schema = m_user_schema; + Obj obj = find_user(*realm, user_id); + if (!obj) { + obj = realm->read_group().get_table(m_user_schema.table_key)->create_object(); + obj.set(schema.user_id_col, user_id); + + // Mark the user we just created as the current user + Obj current_user = current_user_obj(*realm); + current_user.set(m_current_user_schema.user_id, user_id); + } + + obj.set(schema.state_col, (int64_t)UserState::LoggedIn); + obj.set(schema.refresh_token_col, refresh_token); + obj.set(schema.access_token_col, access_token); + obj.set(schema.device_id_col, device_id); + + realm->commit_transaction(); + } + + void update_user(std::string_view user_id, const SyncUserData& user_data, const app::UserData& app_data) override + { + auto realm = get_realm(); + realm->begin_transaction(); + auto& schema = m_user_schema; + Obj obj = find_user(*realm, user_id); + REALM_ASSERT(obj); + obj.set(schema.state_col, (int64_t)user_data.state); + obj.set(schema.refresh_token_col, user_data.refresh_token.token); + obj.set(schema.access_token_col, user_data.access_token.token); + obj.set(schema.device_id_col, app_data.device_id); + + std::stringstream profile; + profile << app_data.profile.data(); + obj.set(schema.profile_dump_col, profile.str()); + + auto identities_list = obj.get_linklist(schema.identities_col); + identities_list.clear(); + + for (auto& ident : app_data.identities) { + auto obj = identities_list.create_and_insert_linked_object(identities_list.size()); + obj.set(m_user_identity_schema.user_id, ident.id); + obj.set(m_user_identity_schema.provider_id, ident.provider_type); + } + + // intentionally does not update `legacy_identities` as that field is + // read-only and no longer used + + realm->commit_transaction(); + } + + Obj current_user_obj(Realm& realm) const + { + TableRef current_user_table = realm.read_group().get_table(m_current_user_schema.table_key); + Obj obj; + if (!current_user_table->is_empty()) + obj = *current_user_table->begin(); + else if (realm.is_in_transaction()) + obj = current_user_table->create_object(); + return obj; + } + + // Some of our string columns are nullable. They never should actually be + // null as we store "" rather than null when the value isn't present, but + // be safe and handle it anyway. + static std::string get_string(const Obj& obj, ColKey col) + { + auto str = obj.get(col); + return str.is_null() ? "" : str; + } + + Data read_user(const Obj& obj) const + { + Data data; + data.first.access_token = RealmJWT(get_string(obj, m_user_schema.access_token_col)); + data.first.refresh_token = RealmJWT(get_string(obj, m_user_schema.refresh_token_col)); + data.second.device_id = get_string(obj, m_user_schema.device_id_col); + if (auto profile = obj.get(m_user_schema.profile_dump_col); profile.size()) { + data.second.profile = static_cast(bson::parse(std::string_view(profile))); + } + + auto identities_list = obj.get_linklist(m_user_schema.identities_col); + auto identities_table = identities_list.get_target_table(); + data.second.identities.reserve(identities_list.size()); + for (size_t i = 0, size = identities_list.size(); i < size; ++i) { + auto obj = identities_table->get_object(identities_list.get(i)); + data.second.identities.push_back({obj.get(m_user_identity_schema.user_id), + obj.get(m_user_identity_schema.provider_id)}); + } + + auto legacy_identities = obj.get_list(m_user_schema.legacy_uuids_col); + data.first.legacy_identities.reserve(legacy_identities.size()); + for (size_t i = 0, size = legacy_identities.size(); i < size; ++i) { + data.first.legacy_identities.push_back(legacy_identities.get(i)); + } + + return data; + } + + void log_out(std::string_view user_id, UserState new_state) override + { + REALM_ASSERT(new_state != UserState::LoggedIn); + auto realm = get_realm(); + realm->begin_transaction(); + if (auto obj = find_user(*realm, user_id)) { + obj.set(m_user_schema.state_col, (int64_t)new_state); + obj.set(m_user_schema.access_token_col, ""); + obj.set(m_user_schema.refresh_token_col, ""); + + auto current_user = current_user_obj(*realm); + if (current_user.get(m_current_user_schema.user_id) == user_id) { + current_user.set(m_current_user_schema.user_id, ""); + } + } + realm->commit_transaction(); + } + + void delete_user(SyncFileManager& file_manager, std::string_view user_id) override + { + auto realm = get_realm(); + realm->begin_transaction(); + if (auto obj = find_user(*realm, user_id)) { + delete_user_realms(file_manager, obj); + auto current_user = current_user_obj(*realm); + if (current_user.get(m_current_user_schema.user_id) == user_id) { + current_user.set(m_current_user_schema.user_id, ""); + } + obj.remove(); + } + realm->commit_transaction(); + } + + void add_realm_path(std::string_view user_id, std::string_view path) override + { + auto realm = get_realm(); + realm->begin_transaction(); + auto obj = find_user(*realm, user_id); + REALM_ASSERT(obj); + obj.get_set(m_user_schema.realm_file_paths_col).insert(path); + realm->commit_transaction(); + } + + bool is_valid_user(Obj& obj) + { + return obj && obj.get(m_user_schema.access_token_col).size() && + obj.get(m_user_schema.refresh_token_col).size() && + obj.get(m_user_schema.state_col) == int64_t(UserState::LoggedIn); + } + + std::vector get_logged_in_users() override + { + auto realm = get_realm(); + auto table = realm->read_group().get_table(m_user_schema.table_key); + std::vector users; + users.reserve(table->size()); + for (auto& obj : *table) { + if (is_valid_user(obj)) { + users.emplace_back(obj.get(m_user_schema.user_id_col)); + } + } + return users; + } + + std::string get_current_user() override + { + auto realm = get_realm(); + auto obj = current_user_obj(*realm); + if (obj && obj.get(m_current_user_schema.user_id).size()) + return obj.get(m_current_user_schema.user_id); + auto table = realm->read_group().get_table(m_user_schema.table_key); + for (auto& obj : *table) { + if (is_valid_user(obj)) { + return obj.get(m_user_schema.user_id_col); + } + } + return ""; + } + + void set_current_user(std::string_view user_id) override + { + auto realm = get_realm(); + realm->begin_transaction(); + current_user_obj(*realm).set(m_current_user_schema.user_id, user_id); + realm->commit_transaction(); + } + + void create_file_action(SyncFileAction action, std::string_view original_path, std::string_view recovery_path, + std::string_view partition_value, std::string_view user_id) override + { + auto realm = get_realm(); + realm->begin_transaction(); + TableRef table = realm->read_group().get_table(m_file_action_schema.table_key); + Obj obj = table->create_object_with_primary_key(original_path); + obj.set(m_file_action_schema.idx_new_name, recovery_path); + obj.set(m_file_action_schema.idx_action, static_cast(action)); + obj.set(m_file_action_schema.idx_partition, partition_value); + obj.set(m_file_action_schema.idx_user_identity, user_id); + realm->commit_transaction(); + } + + + Obj find_user(Realm& realm, StringData user_id) const + { + auto table = realm.read_group().get_table(m_user_schema.table_key); + Query q = table->where().equal(m_user_schema.user_id_col, user_id); + REALM_ASSERT_DEBUG(q.count() < 2); // user_id_col ought to be a primary key + Obj obj; + if (auto key = q.find()) + obj = table->get_object(key); + return obj; + } +}; + +class InMemoryMetadataStorage : public app::MetadataStore { + std::map> m_users; + std::map, std::less<>> m_realm_paths; + std::string m_active_user; + + bool has_user(std::string_view user_id) override + { + auto it = m_users.find(user_id); + return it != m_users.end() && it->second.first.state == UserState::LoggedIn; + } + + Data get_user(std::string_view user_id) override + { + return m_users.find(user_id)->second; + } + + void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token, + std::string_view device_id) override + { + auto it = m_users.find(user_id); + if (it == m_users.end()) { + it = m_users.insert({std::string(user_id), Data{}}).first; + m_active_user = user_id; + } + auto& user = it->second; + user.first.refresh_token = RealmJWT(refresh_token); + user.first.access_token = RealmJWT(access_token); + user.second.device_id = device_id; + user.first.state = UserState::LoggedIn; + } + + void update_user(std::string_view user_id, const SyncUserData& sync_data, const app::UserData& app_data) override + { + auto& user = m_users.find(user_id)->second; + user.first = sync_data; + user.second = app_data; + } + + void log_out(std::string_view user_id, UserState new_state) override + { + if (auto it = m_users.find(user_id); it != m_users.end()) { + auto& user = it->second; + user.first.state = new_state; + user.first.access_token = {}; + user.first.refresh_token = {}; + user.second.device_id.clear(); + if (m_active_user == user_id) + m_active_user = ""; + } + } + + void delete_user(SyncFileManager& file_manager, std::string_view user_id) override + { + if (auto it = m_users.find(user_id); it != m_users.end()) { + m_users.erase(it); + if (m_active_user == user_id) + m_active_user.clear(); + } + if (auto it = m_realm_paths.find(user_id); it != m_realm_paths.end()) { + for (auto& path : it->second) { + file_manager.remove_realm(path); + } + } + } + + std::string get_current_user() override + { + return m_active_user; + } + + void set_current_user(std::string_view user_id) override + { + m_active_user = user_id; + } + + std::vector get_logged_in_users() override + { + std::vector users; + for (auto& [user_id, _] : m_users) + users.push_back(user_id); + return users; + } + + void add_realm_path(std::string_view user_id, std::string_view path) override + { + m_realm_paths[std::string(user_id)].insert(std::string(path)); + } + + bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view realm_path) override + { + return false; + } + + void create_file_action(SyncFileAction action, std::string_view original_path, std::string_view recovery_path, + std::string_view partition_value, std::string_view user_id) override + { + } +}; + +} // anonymous namespace + +app::MetadataStore::~MetadataStore() = default; + +std::unique_ptr app::create_metadata_store(const AppConfig& config, SyncFileManager& file_manager) +{ + if (config.metadata_mode == AppConfig::MetadataMode::InMemory) { + return std::make_unique(); + } + return std::make_unique( + file_manager.metadata_path(), config.metadata_mode != AppConfig::MetadataMode::NoEncryption, + config.custom_encryption_key, file_manager); +} diff --git a/src/realm/object-store/sync/impl/app_metadata.hpp b/src/realm/object-store/sync/impl/app_metadata.hpp new file mode 100644 index 00000000000..96a01aebf5e --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.hpp @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_APP_BACKING_STORE_HPP +#define REALM_OS_APP_BACKING_STORE_HPP + +#include +#include +#include + +#include +#include +#include +#include + +namespace realm { +class SyncFileManager; + +namespace app { +class App; + +class MetadataStore { +public: + using Data = std::pair; + + virtual ~MetadataStore(); + + // Attempt to perform all pending file actions for the given path. Returns + // true if any were performed. + virtual bool immediately_run_file_actions(SyncFileManager& fm, std::string_view realm_path) = 0; + + virtual void create_file_action(SyncFileAction action, std::string_view original_path, + std::string_view recovery_path, std::string_view partition_value, + std::string_view user_id) = 0; + + virtual bool has_user(std::string_view user_id) = 0; + virtual Data get_user(std::string_view user_id) = 0; + + // Create a user if no user with this id exists, or update only the given + // fields if one does + virtual void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token, + std::string_view device_id) = 0; + + // Update the stored data for an existing user + virtual void update_user(std::string_view user_id, const SyncUserData&, const UserData& data) = 0; + + // Discard tokens, set state to the given one, and if the user is the current + // user set it to the new active user + virtual void log_out(std::string_view user_id, UserState new_state) = 0; + virtual void delete_user(SyncFileManager& file_manager, std::string_view user_id) = 0; + + virtual std::string get_current_user() = 0; + virtual void set_current_user(std::string_view user_id) = 0; + + virtual std::vector get_logged_in_users() = 0; + + virtual void add_realm_path(std::string_view user_id, std::string_view path) = 0; +}; + +std::unique_ptr create_metadata_store(const AppConfig& config, SyncFileManager& file_manager); + +} // namespace app +} // namespace realm + +#endif // REALM_OS_APP_BACKING_STORE_HPP diff --git a/src/realm/object-store/sync/impl/sync_client.hpp b/src/realm/object-store/sync/impl/sync_client.hpp index 73abfa1b10f..6875158f962 100644 --- a/src/realm/object-store/sync/impl/sync_client.hpp +++ b/src/realm/object-store/sync/impl/sync_client.hpp @@ -37,8 +37,7 @@ #include #endif -namespace realm { -namespace _impl { +namespace realm::_impl { struct SyncClient { SyncClient(const std::shared_ptr& logger, SyncClientConfig const& config, @@ -138,8 +137,6 @@ struct SyncClient { return m_client.notify_session_terminated(); } - ~SyncClient() {} - private: std::shared_ptr m_socket_provider; sync::Client m_client; @@ -150,7 +147,6 @@ struct SyncClient { #endif }; -} // namespace _impl -} // namespace realm +} // namespace realm::_impl #endif // REALM_OS_SYNC_CLIENT_HPP diff --git a/src/realm/object-store/sync/impl/sync_file.cpp b/src/realm/object-store/sync/impl/sync_file.cpp index 147d1f47e2e..2c25195ef74 100644 --- a/src/realm/object-store/sync/impl/sync_file.cpp +++ b/src/realm/object-store/sync/impl/sync_file.cpp @@ -18,7 +18,10 @@ #include +#include + #include +#include #include #include #include @@ -253,21 +256,21 @@ std::string SyncFileManager::get_special_directory(std::string directory_name) c return dir_path; } -std::string SyncFileManager::user_directory(const std::string& user_identity) const +std::string SyncFileManager::user_directory(const std::string& user_id) const { - std::string user_path = get_user_directory_path(user_identity); + std::string user_path = get_user_directory_path(user_id); util::try_make_dir(user_path); return user_path; } -void SyncFileManager::remove_user_realms(const std::string& user_identity, +void SyncFileManager::remove_user_realms(const std::string& user_id, const std::vector& realm_paths) const { for (auto& path : realm_paths) { remove_realm(path); } // The following is redundant except for apps built before file tracking. - std::string user_path = get_user_directory_path(user_identity); + std::string user_path = get_user_directory_path(user_id); util::try_remove_dir_recursive(user_path); } @@ -300,11 +303,10 @@ bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::st return true; } -bool SyncFileManager::remove_realm(const std::string& user_identity, - const std::vector& legacy_user_identities, +bool SyncFileManager::remove_realm(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& raw_realm_path, const std::string& partition) const { - auto existing = get_existing_realm_file_path(user_identity, legacy_user_identities, raw_realm_path, partition); + auto existing = get_existing_realm_file_path(user_id, legacy_user_identities, raw_realm_path, partition); if (existing) { return remove_realm(*existing); } @@ -333,11 +335,11 @@ static bool try_file_remove(const std::string& path) noexcept } util::Optional -SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, +SyncFileManager::get_existing_realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const { - std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name); + std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_id, realm_file_name); if (try_file_exists(preferred_name_without_suffix)) { return preferred_name_without_suffix; } @@ -364,7 +366,7 @@ SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, // We used to hash the string value of the partition. For compatibility, check that SHA256 // hash file name exists, and if it does, continue to use it. if (!partition.empty()) { - std::string hashed_partition_path = legacy_hashed_partition_path(user_identity, partition); + std::string hashed_partition_path = legacy_hashed_partition_path(user_id, partition); if (try_file_exists(hashed_partition_path)) { return hashed_partition_path; } @@ -386,12 +388,11 @@ SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, return util::none; } -std::string SyncFileManager::realm_file_path(const std::string& user_identity, +std::string SyncFileManager::realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const { - auto existing_path = - get_existing_realm_file_path(user_identity, legacy_user_identities, realm_file_name, partition); + auto existing_path = get_existing_realm_file_path(user_id, legacy_user_identities, realm_file_name, partition); if (existing_path) { return *existing_path; } @@ -399,7 +400,7 @@ std::string SyncFileManager::realm_file_path(const std::string& user_identity, // since this appears to be a new file, test the normal location // we use a test file with the same name and a suffix of the // same length, so we can catch "filename too long" errors on windows - std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name); + std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_id, realm_file_name); std::string preferred_name_with_suffix = preferred_name_without_suffix + c_realm_file_suffix; try { std::string test_path = preferred_name_without_suffix + c_realm_file_test_suffix; @@ -455,12 +456,11 @@ bool SyncFileManager::remove_metadata_realm() const } } -std::string SyncFileManager::preferred_realm_path_without_suffix(const std::string& user_identity, +std::string SyncFileManager::preferred_realm_path_without_suffix(const std::string& user_id, const std::string& realm_file_name) const { auto escaped_file_name = util::validate_and_clean_path(realm_file_name); - std::string preferred_name = - util::file_path_by_appending_component(user_directory(user_identity), escaped_file_name); + std::string preferred_name = util::file_path_by_appending_component(user_directory(user_id), escaped_file_name); if (StringData(preferred_name).ends_with(c_realm_file_suffix)) { preferred_name = preferred_name.substr(0, preferred_name.size() - strlen(c_realm_file_suffix)); } @@ -476,14 +476,14 @@ std::string SyncFileManager::fallback_hashed_realm_file_path(const std::string& return hashed_name; } -std::string SyncFileManager::legacy_hashed_partition_path(const std::string& user_identity, +std::string SyncFileManager::legacy_hashed_partition_path(const std::string& user_id, const std::string& partition) const { std::array hash; util::sha256(partition.data(), partition.size(), hash.data()); std::string legacy_hashed_file_name = util::hex_dump(hash.data(), hash.size(), ""); std::string legacy_partition_path = util::file_path_by_appending_component( - get_user_directory_path(user_identity), legacy_hashed_file_name + c_realm_file_suffix); + get_user_directory_path(user_id), legacy_hashed_file_name + c_realm_file_suffix); return legacy_partition_path; } @@ -509,10 +509,76 @@ std::string SyncFileManager::legacy_local_identity_path(const std::string& local return path; } -std::string SyncFileManager::get_user_directory_path(const std::string& user_identity) const +std::string SyncFileManager::get_user_directory_path(const std::string& user_id) const { - return file_path_by_appending_component(m_app_path, util::validate_and_clean_path(user_identity), + return file_path_by_appending_component(m_app_path, util::validate_and_clean_path(user_id), util::FilePathType::Directory); } +struct UnsupportedBsonPartition : public std::logic_error { + UnsupportedBsonPartition(std::string msg) + : std::logic_error(msg) + { + } +}; + +static std::string string_from_partition(std::string_view partition) +{ + bson::Bson partition_value = bson::parse(partition); + switch (partition_value.type()) { + case bson::Bson::Type::Int32: + return util::format("i_%1", static_cast(partition_value)); + case bson::Bson::Type::Int64: + return util::format("l_%1", static_cast(partition_value)); + case bson::Bson::Type::String: + return util::format("s_%1", static_cast(partition_value)); + case bson::Bson::Type::ObjectId: + return util::format("o_%1", static_cast(partition_value).to_string()); + case bson::Bson::Type::Uuid: + return util::format("u_%1", static_cast(partition_value).to_string()); + case bson::Bson::Type::Null: + return "null"; + default: + throw UnsupportedBsonPartition(util::format("Unsupported partition key value: '%1'. Only int, string " + "UUID and ObjectId types are currently supported.", + partition_value.to_string())); + } +} + +std::string SyncFileManager::path_for_realm(const SyncConfig& config, + std::optional custom_file_name) const +{ + auto user = config.user; + REALM_ASSERT(user); + // Attempt to make a nicer filename which will ease debugging when + // locating files in the filesystem. + auto file_name = [&]() -> std::string { + if (custom_file_name) { + return *custom_file_name; + } + if (config.flx_sync_requested) { + REALM_ASSERT_DEBUG(config.partition_value.empty()); + return "flx_sync_default"; + } + return string_from_partition(config.partition_value); + }(); + auto path = realm_file_path(user->user_id(), user->legacy_identities(), file_name, config.partition_value); + user->track_realm(path); + return path; +} + +std::string SyncFileManager::audit_path_root(const SyncUser& user, std::string_view partition_prefix) const +{ +#ifdef _WIN32 // Move to File? + const char separator[] = "\\"; +#else + const char separator[] = "/"; +#endif + + // "$root/realm-audit/$appId/$userId/$partitonPrefix/" + return util::format("%2%1realm-audit%1%3%1%4%1%5%1", separator, m_base_path, user.app_id(), user.user_id(), + partition_prefix); + return ""; +} + } // namespace realm diff --git a/src/realm/object-store/sync/impl/sync_file.hpp b/src/realm/object-store/sync/impl/sync_file.hpp index 7750ae85748..9310dd6e6da 100644 --- a/src/realm/object-store/sync/impl/sync_file.hpp +++ b/src/realm/object-store/sync/impl/sync_file.hpp @@ -19,13 +19,12 @@ #ifndef REALM_OS_SYNC_FILE_HPP #define REALM_OS_SYNC_FILE_HPP +#include #include -#include - -#include - namespace realm { +struct SyncConfig; +class SyncUser; namespace util { @@ -61,24 +60,33 @@ class SyncFileManager { SyncFileManager(const std::string& base_path, const std::string& app_id); /// Remove the Realms at the specified absolute paths along with any associated helper files. - void remove_user_realms(const std::string& user_identity, + void remove_user_realms(const std::string& user_id, const std::vector& realm_paths) const; // throws /// A non throw version of File::exists(), returning false if any exceptions are thrown when attempting to access /// this file. static bool try_file_exists(const std::string& path) noexcept; - util::Optional get_existing_realm_file_path(const std::string& user_identity, - const std::vector& legacy_user_identities, - const std::string& realm_file_name, - const std::string& partition) const; + std::optional get_existing_realm_file_path(const std::string& user_id, + const std::vector& legacy_user_identities, + const std::string& realm_file_name, + const std::string& partition) const; /// Return the path for a given Realm, creating the user directory if it does not already exist. - std::string realm_file_path(const std::string& user_identity, - const std::vector& legacy_user_identities, + std::string realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; + // Get the default path for a Realm for the given configuration. + // The default value is `///.realm`. + // If the file cannot be created at this location, for example due to path length restrictions, + // this function may pass back `/.realm` + std::string path_for_realm(const SyncConfig& config, + std::optional custom_file_name = std::nullopt) const; + + // Get the base path where audit Realms will be stored. This path may need to be created. + std::string audit_path_root(const SyncUser& user, std::string_view partition_prefix) const; + /// Remove the Realm at a given path for a given user. Returns `true` if the remove operation fully succeeds. - bool remove_realm(const std::string& user_identity, const std::vector& legacy_user_identities, + bool remove_realm(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; /// Remove the Realm whose primary Realm file is located at `absolute_path`. Returns `true` if the remove @@ -104,7 +112,7 @@ class SyncFileManager { return m_app_path; } - std::string recovery_directory_path(util::Optional const& directory = none) const + std::string recovery_directory_path(std::optional const& directory = {}) const { return get_special_directory(directory.value_or(c_recovery_directory)); } @@ -134,15 +142,15 @@ class SyncFileManager { return get_special_directory(c_utility_directory); } /// Return the user directory for a given user, creating it if it does not already exist. - std::string user_directory(const std::string& identity) const; + std::string user_directory(const std::string& user_id) const; // Construct the absolute path to the users directory - std::string get_user_directory_path(const std::string& user_identity) const; - std::string legacy_hashed_partition_path(const std::string& user_identity, const std::string& partition) const; + std::string get_user_directory_path(const std::string& user_id) const; + std::string legacy_hashed_partition_path(const std::string& user_id, const std::string& partition) const; std::string legacy_realm_file_path(const std::string& local_user_identity, const std::string& realm_file_name) const; std::string legacy_local_identity_path(const std::string& local_user_identity, const std::string& realm_file_name) const; - std::string preferred_realm_path_without_suffix(const std::string& user_identity, + std::string preferred_realm_path_without_suffix(const std::string& user_id, const std::string& realm_file_name) const; std::string fallback_hashed_realm_file_path(const std::string& preferred_path) const; }; diff --git a/src/realm/object-store/sync/impl/sync_metadata.cpp b/src/realm/object-store/sync/impl/sync_metadata.cpp deleted file mode 100644 index 8fc53c30da6..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ /dev/null @@ -1,828 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#if REALM_PLATFORM_APPLE -#include -#endif - -#include -#include -#include - -using namespace realm; - -namespace { -static const char* const c_sync_userMetadata = "UserMetadata"; -static const char* const c_sync_identityMetadata = "UserIdentity"; -static const char* const c_sync_app_metadata = "AppMetadata"; - -static const char* const c_sync_current_user_identity = "current_user_identity"; - -/* User keys */ -static const char* const c_sync_identity = "identity"; -static const char* const c_sync_legacy_uuids = "legacy_uuids"; -static const char* const c_sync_refresh_token = "refresh_token"; -static const char* const c_sync_access_token = "access_token"; -static const char* const c_sync_identities = "identities"; -static const char* const c_sync_state = "state"; -static const char* const c_sync_device_id = "device_id"; -static const char* const c_sync_profile_data = "profile_data"; -static const char* const c_sync_local_realm_paths = "local_realm_paths"; - -/* Identity keys */ -static const char* const c_sync_user_id = "id"; -static const char* const c_sync_provider_type = "provider_type"; - -static const char* const c_sync_fileActionMetadata = "FileActionMetadata"; -static const char* const c_sync_original_name = "original_name"; -static const char* const c_sync_new_name = "new_name"; -static const char* const c_sync_action = "action"; -static const char* const c_sync_partition = "url"; - -static const char* const c_sync_app_metadata_id = "id"; -static const char* const c_sync_app_metadata_deployment_model = "deployment_model"; -static const char* const c_sync_app_metadata_location = "location"; -static const char* const c_sync_app_metadata_hostname = "hostname"; -static const char* const c_sync_app_metadata_ws_hostname = "ws_hostname"; - -realm::Schema make_schema() -{ - using namespace realm; - return Schema{ - {c_sync_identityMetadata, - ObjectSchema::ObjectType::Embedded, - { - {c_sync_user_id, PropertyType::String}, - {c_sync_provider_type, PropertyType::String}, - }}, - {c_sync_userMetadata, - {{c_sync_identity, PropertyType::String}, - {c_sync_legacy_uuids, PropertyType::String | PropertyType::Array}, - {c_sync_refresh_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_access_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_identities, PropertyType::Object | PropertyType::Array, c_sync_identityMetadata}, - {c_sync_state, PropertyType::Int}, - {c_sync_device_id, PropertyType::String}, - {c_sync_profile_data, PropertyType::String}, - {c_sync_local_realm_paths, PropertyType::Set | PropertyType::String}}}, - {c_sync_fileActionMetadata, - { - {c_sync_original_name, PropertyType::String, Property::IsPrimary{true}}, - {c_sync_new_name, PropertyType::String | PropertyType::Nullable}, - {c_sync_action, PropertyType::Int}, - {c_sync_partition, PropertyType::String}, - {c_sync_identity, PropertyType::String}, - }}, - {c_sync_current_user_identity, - { - {c_sync_current_user_identity, PropertyType::String}, - }}, - {c_sync_app_metadata, - { - {c_sync_app_metadata_id, PropertyType::Int, Property::IsPrimary{true}}, - {c_sync_app_metadata_deployment_model, PropertyType::String}, - {c_sync_app_metadata_location, PropertyType::String}, - {c_sync_app_metadata_hostname, PropertyType::String}, - {c_sync_app_metadata_ws_hostname, PropertyType::String}, - }}, - }; -} - -void migrate_to_v7(std::shared_ptr old_realm, std::shared_ptr realm) -{ - // Before schema version 7 there may have been multiple UserMetadata entries - // for a single user_id with different provider types, so we need to merge - // any duplicates together - - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), c_sync_userMetadata); - if (table->is_empty()) - return; - REALM_ASSERT(table->size() == old_table->size()); - - ColKey id_col = table->get_column_key(c_sync_identity); - ColKey old_uuid_col = old_table->get_column_key("local_uuid"); - ColKey new_uuid_col = table->get_column_key(c_sync_legacy_uuids); - ColKey state_col = table->get_column_key(c_sync_state); - - std::unordered_map users; - for (size_t i = 0, j = 0; i < table->size(); ++j) { - auto obj = table->get_object(i); - - // Move the local uuid from the old column to the list - auto old_obj = old_table->get_object(j); - obj.get_list(new_uuid_col).add(old_obj.get(old_uuid_col)); - - // Check if we've already seen an object with the same id. If not, store - // this one and move on - std::string user_id = obj.get(id_col); - auto& existing = users[obj.get(id_col)]; - if (!existing.is_valid()) { - existing = obj; - ++i; - continue; - } - - // We have a second object for the same id, so we need to merge them. - // First we merge the state: if one is logged in and the other isn't, - // we'll use the logged-in state and tokens. If both are logged in, we'll - // use the more recent login. If one is logged out and the other is - // removed we'll use the logged out state. If both are logged out or - // both are removed then it doesn't matter which we pick. - using State = SyncUser::State; - auto state = State(obj.get(state_col)); - auto existing_state = State(existing.get(state_col)); - if (state == existing_state) { - if (state == State::LoggedIn) { - RealmJWT token_1(existing.get(c_sync_access_token)); - RealmJWT token_2(obj.get(c_sync_access_token)); - if (token_1.issued_at < token_2.issued_at) { - existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); - existing.set(c_sync_access_token, obj.get(c_sync_access_token)); - } - } - } - else if (state == State::LoggedIn || existing_state == State::Removed) { - existing.set(c_sync_state, int64_t(state)); - existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); - existing.set(c_sync_access_token, obj.get(c_sync_access_token)); - } - - // Next we merge the list properties (identities, legacy uuids, realm file paths) - { - auto dest = existing.get_linklist(c_sync_identities); - auto src = obj.get_linklist(c_sync_identities); - for (size_t i = 0, size = src.size(); i < size; ++i) { - if (dest.find_first(src.get(i)) == npos) { - dest.add(src.get(i)); - } - } - } - { - auto dest = existing.get_list(c_sync_legacy_uuids); - auto src = obj.get_list(c_sync_legacy_uuids); - for (size_t i = 0, size = src.size(); i < size; ++i) { - if (dest.find_first(src.get(i)) == npos) { - dest.add(src.get(i)); - } - } - } - { - auto dest = existing.get_set(c_sync_local_realm_paths); - auto src = obj.get_set(c_sync_local_realm_paths); - for (size_t i = 0, size = src.size(); i < size; ++i) { - dest.insert(src.get(i)); - } - } - - - // Finally we delete the duplicate object. We don't increment `i` as it's - // now the index of the object just after the one we're deleting. - obj.remove(); - } -} - -} // anonymous namespace - -// MARK: - Sync metadata manager - -SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key) -{ - constexpr uint64_t SCHEMA_VERSION = 7; - - if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) - throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); - - m_metadata_config.automatic_change_notifications = false; - m_metadata_config.path = path; - m_metadata_config.schema = make_schema(); - m_metadata_config.schema_version = SCHEMA_VERSION; - m_metadata_config.schema_mode = SchemaMode::Automatic; - m_metadata_config.scheduler = util::Scheduler::make_dummy(); - if (encryption_key) - m_metadata_config.encryption_key = std::move(*encryption_key); - m_metadata_config.automatically_handle_backlinks_in_migrations = true; - m_metadata_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, - Schema&) { - if (old_realm->schema_version() < 7) { - migrate_to_v7(old_realm, realm); - } - }; - - auto realm = open_realm(should_encrypt, encryption_key != none); - - // Get data about the (hardcoded) schemas - auto object_schema = realm->schema().find(c_sync_userMetadata); - m_user_schema = { - object_schema->persisted_properties[0].column_key, object_schema->persisted_properties[1].column_key, - object_schema->persisted_properties[2].column_key, object_schema->persisted_properties[3].column_key, - object_schema->persisted_properties[4].column_key, object_schema->persisted_properties[5].column_key, - object_schema->persisted_properties[6].column_key, object_schema->persisted_properties[7].column_key, - object_schema->persisted_properties[8].column_key}; - - object_schema = realm->schema().find(c_sync_fileActionMetadata); - m_file_action_schema = { - object_schema->persisted_properties[0].column_key, object_schema->persisted_properties[1].column_key, - object_schema->persisted_properties[2].column_key, object_schema->persisted_properties[3].column_key, - object_schema->persisted_properties[4].column_key, - }; - - object_schema = realm->schema().find(c_sync_app_metadata); - m_app_metadata_schema = { - object_schema->persisted_properties[0].column_key, object_schema->persisted_properties[1].column_key, - object_schema->persisted_properties[2].column_key, object_schema->persisted_properties[3].column_key, - object_schema->persisted_properties[4].column_key}; -} - -SyncUserMetadataResults SyncMetadataManager::all_unmarked_users() const -{ - return get_users(false); -} - -SyncUserMetadataResults SyncMetadataManager::all_users_marked_for_removal() const -{ - return get_users(true); -} - -SyncUserMetadataResults SyncMetadataManager::get_users(bool marked) const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query; - if (marked) { - query = table->where().equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); - } - else { - query = table->where().not_equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); - } - return SyncUserMetadataResults(Results(realm, std::move(query)), m_user_schema); -} - -util::Optional SyncMetadataManager::get_current_user_identity() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - if (!table->is_empty()) { - auto first = table->begin(); - return util::Optional(first->get(c_sync_current_user_identity)); - } - - return util::Optional(); -} - -SyncFileActionMetadataResults SyncMetadataManager::all_pending_actions() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - return SyncFileActionMetadataResults(Results(realm, table), m_file_action_schema); -} - -void SyncMetadataManager::set_current_user_identity(const std::string& identity) -{ - auto realm = get_realm(); - - realm->begin_transaction(); - - TableRef currentUserIdentityTable = - ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - Obj currentUserIdentityObj; - if (currentUserIdentityTable->is_empty()) - currentUserIdentityObj = currentUserIdentityTable->create_object(); - else - currentUserIdentityObj = *currentUserIdentityTable->begin(); - - currentUserIdentityObj.set(c_sync_current_user_identity, identity); - - realm->commit_transaction(); -} - -util::Optional SyncMetadataManager::get_or_make_user_metadata(const std::string& identity, - bool make_if_absent) const -{ - auto realm = get_realm(); - auto& schema = m_user_schema; - - // Retrieve or create the row for this object. - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query = table->where().equal(schema.identity_col, StringData(identity)); - Results results(realm, std::move(query)); - REALM_ASSERT_DEBUG(results.size() < 2); - auto obj = results.first(); - - if (!obj) { - if (!make_if_absent) - return none; - - realm->begin_transaction(); - // Check the results again. - obj = results.first(); - } - if (!obj) { - // Because "making this user" is our last action, set this new user as the current user - TableRef currentUserIdentityTable = - ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - Obj currentUserIdentityObj; - if (currentUserIdentityTable->is_empty()) - currentUserIdentityObj = currentUserIdentityTable->create_object(); - else - currentUserIdentityObj = *currentUserIdentityTable->begin(); - - obj = table->create_object(); - - currentUserIdentityObj.set(c_sync_current_user_identity, identity); - - obj->set(schema.identity_col, identity); - obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); - realm->commit_transaction(); - return SyncUserMetadata(schema, std::move(realm), *obj); - } - - // Got an existing user. - if (obj->get(schema.state_col) == int64_t(SyncUser::State::Removed)) { - // User is dead. Revive or return none. - if (!make_if_absent) { - return none; - } - - if (!realm->is_in_transaction()) - realm->begin_transaction(); - obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); - realm->commit_transaction(); - } - - return SyncUserMetadata(schema, std::move(realm), std::move(*obj)); -} - -void SyncMetadataManager::make_file_action_metadata(StringData original_name, StringData partition_key_value, - StringData local_uuid, SyncFileActionMetadata::Action action, - StringData new_name) const -{ - // This function can't use get_shared_realm() because it's called on a - // background thread and that's currently not supported by the libuv - // implementation of EventLoopSignal - auto coordinator = _impl::RealmCoordinator::get_coordinator(m_metadata_config); - auto group_ptr = coordinator->begin_read(); - auto& group = *group_ptr; - REALM_ASSERT(typeid(group) == typeid(Transaction)); - auto& transaction = static_cast(group); - transaction.promote_to_write(); - - // Retrieve or create the row for this object. - TableRef table = ObjectStore::table_for_object_type(group, c_sync_fileActionMetadata); - - auto& schema = m_file_action_schema; - Obj obj = table->create_object_with_primary_key(original_name); - - obj.set(schema.idx_new_name, new_name); - obj.set(schema.idx_action, static_cast(action)); - obj.set(schema.idx_partition, partition_key_value); - obj.set(schema.idx_user_identity, local_uuid); - transaction.commit(); -} - -util::Optional SyncMetadataManager::get_file_action_metadata(StringData original_name) const -{ - auto realm = get_realm(); - auto& schema = m_file_action_schema; - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - auto row_idx = table->find_first_string(schema.idx_original_name, original_name); - if (!row_idx) - return none; - - return SyncFileActionMetadata(std::move(schema), std::move(realm), table->get_object(row_idx)); -} - -std::shared_ptr SyncMetadataManager::get_realm() const -{ - auto realm = Realm::get_shared_realm(m_metadata_config); - realm->refresh(); - return realm; -} - -std::shared_ptr SyncMetadataManager::try_get_realm() const -{ - try { - return get_realm(); - } - catch (const InvalidDatabase&) { - return nullptr; - } -} - -std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool caller_supplied_key) -{ - if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { - if (auto realm = try_get_realm()) - return realm; - - // Encryption key changed, so delete the existing metadata realm and - // recreate it - util::File::remove(m_metadata_config.path); - return get_realm(); - } - -#if REALM_PLATFORM_APPLE - // This logic is all a giant race condition once we have multi-process sync. - // Wrapping it all (including the keychain accesses) in DB::call_with_lock() - // might suffice. - - // First try to open the Realm with a key already stored in the keychain. - // This works for both the case where everything is sensible and valid and - // when we have a key but no metadata Realm. - auto key = keychain::get_existing_metadata_realm_key(); - if (key) { - m_metadata_config.encryption_key = *key; - if (auto realm = try_get_realm()) - return realm; - } - - // If we have an existing file and either no key or the key didn't work to - // decrypt it, then we might have an unencrypted metadata Realm resulting - // from a previous run being unable to access the keychain. - if (util::File::exists(m_metadata_config.path)) { - m_metadata_config.encryption_key.clear(); - if (auto realm = try_get_realm()) - return realm; - - // We weren't able to open the existing file with either the stored key - // or no key, so just delete it. - util::File::remove(m_metadata_config.path); - } - - // We now have no metadata Realm. If we don't have an existing stored key, - // try to create and store a new one. This might fail, in which case we - // just create an unencrypted Realm file. - if (!key) - key = keychain::create_new_metadata_realm_key(); - if (key) - m_metadata_config.encryption_key = std::move(*key); - return get_realm(); -#else // REALM_PLATFORM_APPLE - REALM_UNREACHABLE(); -#endif // REALM_PLATFORM_APPLE -} - -/// Magic key to fetch app metadata, which there should always only be one of. -static const auto app_metadata_pk = 1; - -bool SyncMetadataManager::set_app_metadata(const std::string& deployment_model, const std::string& location, - const std::string& hostname, const std::string& ws_hostname) -{ - if (m_app_metadata && m_app_metadata->hostname == hostname && m_app_metadata->ws_hostname == ws_hostname && - m_app_metadata->deployment_model == deployment_model && m_app_metadata->location == location) { - // App metadata not updated - return false; - } - - auto realm = get_realm(); - auto& schema = m_app_metadata_schema; - - // let go of stale cached copy of metadata - it will be refreshed on the next call to get_app_metadata() - m_app_metadata = util::none; - - realm->begin_transaction(); - - auto table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_app_metadata); - auto obj = table->create_object_with_primary_key(app_metadata_pk); - obj.set(schema.deployment_model_col, deployment_model); - obj.set(schema.location_col, location); - obj.set(schema.hostname_col, hostname); - obj.set(schema.ws_hostname_col, ws_hostname); - - realm->commit_transaction(); - // App metadata was updated - return true; -} - -util::Optional SyncMetadataManager::get_app_metadata() -{ - if (!m_app_metadata) { - auto realm = get_realm(); - auto table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_app_metadata); - if (!table->size()) - return util::none; - - auto obj = table->get_object_with_primary_key(app_metadata_pk); - auto& schema = m_app_metadata_schema; - m_app_metadata = - SyncAppMetadata{obj.get(schema.deployment_model_col), obj.get(schema.location_col), - obj.get(schema.hostname_col), obj.get(schema.ws_hostname_col)}; - } - - return m_app_metadata; -} - -// MARK: - Sync user metadata - -SyncUserMetadata::SyncUserMetadata(Schema schema, SharedRealm realm, const Obj& obj) - : m_realm(std::move(realm)) - , m_schema(std::move(schema)) - , m_obj(obj) -{ -} - -std::string SyncUserMetadata::identity() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.identity_col); -} - -SyncUser::State SyncUserMetadata::state() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return SyncUser::State(m_obj.get(m_schema.state_col)); -} - -std::vector SyncUserMetadata::legacy_identities() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - std::vector uuids; - auto list = m_obj.get_list(m_schema.legacy_uuids_col); - for (size_t i = 0, size = list.size(); i < size; ++i) { - uuids.push_back(list.get(i)); - } - return uuids; -} - -std::string SyncUserMetadata::refresh_token() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.refresh_token_col); - return result.is_null() ? "" : std::string(result); -} - -std::string SyncUserMetadata::access_token() const -{ - REALM_ASSERT(m_realm); - StringData result = m_obj.get(m_schema.access_token_col); - return result.is_null() ? "" : std::string(result); -} - -std::string SyncUserMetadata::device_id() const -{ - REALM_ASSERT(m_realm); - StringData result = m_obj.get(m_schema.device_id_col); - return result.is_null() ? "" : std::string(result); -} - -inline SyncUserIdentity user_identity_from_obj(const Obj& obj) -{ - return SyncUserIdentity(obj.get(c_sync_user_id), obj.get(c_sync_provider_type)); -} - -std::vector SyncUserMetadata::identities() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - auto linklist = m_obj.get_linklist(m_schema.identities_col); - - std::vector identities; - for (size_t i = 0; i < linklist.size(); i++) { - auto obj = linklist.get_object(i); - identities.push_back(user_identity_from_obj(obj)); - } - - return identities; -} - -SyncUserProfile SyncUserMetadata::profile() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.profile_dump_col); - if (result.size() == 0) { - return SyncUserProfile(); - } - return SyncUserProfile(static_cast(bson::parse(std::string_view(result)))); -} - -void SyncUserMetadata::set_refresh_token(const std::string& refresh_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.refresh_token_col, refresh_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_state(SyncUser::State state) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.state_col, (int64_t)state); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.state_col, static_cast(state)); - m_obj.set(m_schema.access_token_col, access_token); - m_obj.set(m_schema.refresh_token_col, refresh_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_identities(std::vector identities) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - - auto link_list = m_obj.get_linklist(m_schema.identities_col); - auto identities_table = link_list.get_target_table(); - auto col_user_id = identities_table->get_column_key(c_sync_user_id); - auto col_provider_type = identities_table->get_column_key(c_sync_provider_type); - link_list.clear(); - - for (auto& ident : identities) { - auto obj = link_list.create_and_insert_linked_object(link_list.size()); - obj.set(col_user_id, ident.id); - obj.set(col_provider_type, ident.provider_type); - } - - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_access_token(const std::string& user_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.access_token_col, user_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_device_id(const std::string& device_id) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.device_id_col, device_id); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_legacy_identities(const std::vector& uuids) -{ - m_realm->begin_transaction(); - auto list = m_obj.get_list(m_schema.legacy_uuids_col); - list.clear(); - for (auto& uuid : uuids) - list.add(uuid); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_user_profile(const SyncUserProfile& profile) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - std::stringstream data; - data << profile.data(); - m_obj.set(m_schema.profile_dump_col, data.str()); - m_realm->commit_transaction(); -} - -std::vector SyncUserMetadata::realm_file_paths() const -{ - if (m_invalid) - return {}; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->refresh(); - Set paths = m_obj.get_set(m_schema.realm_file_paths_col); - return std::vector(paths.begin(), paths.end()); -} - -void SyncUserMetadata::add_realm_file_path(const std::string& path) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - Set paths = m_obj.get_set(m_schema.realm_file_paths_col); - paths.insert(path); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::remove() -{ - m_invalid = true; - m_realm->begin_transaction(); - m_obj.remove(); - m_realm->commit_transaction(); - m_realm = nullptr; -} - -// MARK: - File action metadata - -SyncFileActionMetadata::SyncFileActionMetadata(Schema schema, SharedRealm realm, const Obj& obj) - : m_realm(std::move(realm)) - , m_schema(std::move(schema)) - , m_obj(obj) -{ -} - -std::string SyncFileActionMetadata::original_name() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.idx_original_name); -} - -util::Optional SyncFileActionMetadata::new_name() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.idx_new_name); - return result.is_null() ? util::none : util::make_optional(std::string(result)); -} - -std::string SyncFileActionMetadata::user_local_uuid() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.idx_user_identity); -} - -SyncFileActionMetadata::Action SyncFileActionMetadata::action() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return static_cast(m_obj.get(m_schema.idx_action)); -} - -std::string SyncFileActionMetadata::partition() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.idx_partition); -} - -void SyncFileActionMetadata::remove() -{ - REALM_ASSERT(m_realm); - m_realm->begin_transaction(); - m_obj.remove(); - m_realm->commit_transaction(); - m_realm = nullptr; -} - -void SyncFileActionMetadata::set_action(Action new_action) -{ - REALM_ASSERT(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.idx_action, static_cast(new_action)); - m_realm->commit_transaction(); -} diff --git a/src/realm/object-store/sync/impl/sync_metadata.hpp b/src/realm/object-store/sync/impl/sync_metadata.hpp deleted file mode 100644 index 2ab0d84b3cf..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.hpp +++ /dev/null @@ -1,272 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#ifndef REALM_OS_SYNC_METADATA_HPP -#define REALM_OS_SYNC_METADATA_HPP - -#include -#include -#include - -#include -#include -#include -#include - -namespace realm { -class SyncMetadataManager; - -// A facade for a metadata Realm object representing app metadata -class SyncAppMetadata { -public: - struct Schema { - ColKey id_col; - ColKey deployment_model_col; - ColKey location_col; - ColKey hostname_col; - ColKey ws_hostname_col; - }; - - std::string deployment_model; - std::string location; - std::string hostname; - std::string ws_hostname; -}; - -// A facade for a metadata Realm object representing a sync user. -class SyncUserMetadata { -public: - struct Schema { - // The server-supplied user_id for the user. Unique per App. - ColKey identity_col; - // Locally generated UUIDs for the user. These are tracked to be able - // to open pre-existing Realm files, but are no longer generated or - // used for anything else. - ColKey legacy_uuids_col; - // The cached refresh token for this user. - ColKey refresh_token_col; - // The cached access token for this user. - ColKey access_token_col; - // The identities for this user. - ColKey identities_col; - // The current state of this user. - ColKey state_col; - // The device id of this user. - ColKey device_id_col; - // Any additional profile attributes, formatted as a bson string. - ColKey profile_dump_col; - // The set of absolute file paths to Realms belonging to this user. - ColKey realm_file_paths_col; - }; - - // Cannot be set after creation. - std::string identity() const; - - std::vector legacy_identities() const; - // for testing purposes only - void set_legacy_identities(const std::vector&); - - std::vector identities() const; - void set_identities(std::vector); - - void set_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token); - - std::string refresh_token() const; - void set_refresh_token(const std::string& token); - - std::string access_token() const; - void set_access_token(const std::string& token); - - std::string device_id() const; - void set_device_id(const std::string&); - - SyncUserProfile profile() const; - void set_user_profile(const SyncUserProfile&); - - std::vector realm_file_paths() const; - void add_realm_file_path(const std::string& path); - - void set_state(SyncUser::State); - - SyncUser::State state() const; - - void remove(); - - bool is_valid() const - { - return !m_invalid; - } - - // INTERNAL USE ONLY - SyncUserMetadata(Schema schema, SharedRealm realm, const Obj& obj); - -private: - bool m_invalid = false; - SharedRealm m_realm; - Schema m_schema; - Obj m_obj; -}; - -// A facade for a metadata Realm object representing a pending action to be carried out upon a specific file(s). -class SyncFileActionMetadata { -public: - struct Schema { - // The original path on disk of the file (generally, the main file for an on-disk Realm). - ColKey idx_original_name; - // A new path on disk for a file to be written to. Context-dependent. - ColKey idx_new_name; - // An enum describing the action to take. - ColKey idx_action; - // The partition key of the Realm. - ColKey idx_partition; - // The local UUID of the user to whom the file action applies (despite the internal column name). - ColKey idx_user_identity; - }; - - enum class Action { - // The Realm files at the given directory will be deleted. - DeleteRealm, - // The Realm file will be copied to a 'recovery' directory, and the original Realm files will be deleted. - BackUpThenDeleteRealm - }; - - // The absolute path to the Realm file in question. - std::string original_name() const; - - // The meaning of this parameter depends on the `Action` specified. - // For `BackUpThenDeleteRealm`, it is the absolute path where the backup copy - // of the Realm file found at `original_name()` will be placed. - // For all other `Action`s, it is ignored. - util::Optional new_name() const; - - // Get the local UUID of the user associated with this file action metadata. - std::string user_local_uuid() const; - - Action action() const; - std::string partition() const; - void remove(); - void set_action(Action new_action); - - // INTERNAL USE ONLY - SyncFileActionMetadata(Schema schema, SharedRealm realm, const Obj& obj); - -private: - SharedRealm m_realm; - Schema m_schema; - Obj m_obj; -}; - -template -class SyncMetadataResults { -public: - size_t size() const - { - m_results.get_realm()->refresh(); - return m_results.size(); - } - - T get(size_t idx) const - { - m_results.get_realm()->refresh(); - auto row = m_results.get(idx); - return T(m_schema, m_results.get_realm(), row); - } - - SyncMetadataResults(Results results, typename T::Schema schema) - : m_schema(std::move(schema)) - , m_results(std::move(results)) - { - } - -private: - typename T::Schema m_schema; - mutable Results m_results; -}; -using SyncUserMetadataResults = SyncMetadataResults; -using SyncFileActionMetadataResults = SyncMetadataResults; - -// A facade for the application's metadata Realm. -class SyncMetadataManager { - friend class SyncUserMetadata; - friend class SyncFileActionMetadata; - -public: - // Return a Results object containing all users not marked for removal. - SyncUserMetadataResults all_unmarked_users() const; - - // Return a Results object containing all users marked for removal. It is the binding's responsibility to call - // `remove()` on each user to actually remove it from the database. (This is so that already-open Realm files can - // be safely cleaned up the next time the host is launched.) - SyncUserMetadataResults all_users_marked_for_removal() const; - - // Return a Results object containing all pending actions. - SyncFileActionMetadataResults all_pending_actions() const; - - // Retrieve or create user metadata. - // Note: if `make_is_absent` is true and the user has been marked for deletion, it will be unmarked. - util::Optional get_or_make_user_metadata(const std::string& identity, - bool make_if_absent = true) const; - - // Retrieve file action metadata. - util::Optional get_file_action_metadata(StringData path) const; - - // Create file action metadata. - void make_file_action_metadata(StringData original_name, StringData partition_key_value, StringData local_uuid, - SyncFileActionMetadata::Action action, StringData new_name = {}) const; - - util::Optional get_current_user_identity() const; - void set_current_user_identity(const std::string& identity); - - util::Optional get_app_metadata(); - /// Set or update the cached app server metadata. The metadata will not be updated if it has already been - /// set and the provided values are not different than the cached information. Returns true if the metadata - /// was updated. - /// @param deployment_model The deployment model reported by the app server - /// @param location The location name where the app server is located - /// @param hostname The hostname to use for the app server admin api - /// @param ws_hostname The hostname to use for the app server websocket connections - bool set_app_metadata(const std::string& deployment_model, const std::string& location, - const std::string& hostname, const std::string& ws_hostname); - - /// Construct the metadata manager. - /// - /// If the platform supports it, setting `should_encrypt` to `true` and not specifying an encryption key will make - /// the object store handle generating and persisting an encryption key for the metadata database. Otherwise, an - /// exception will be thrown. - SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key = none); - -private: - SyncUserMetadataResults get_users(bool marked) const; - Realm::Config m_metadata_config; - SyncUserMetadata::Schema m_user_schema; - SyncFileActionMetadata::Schema m_file_action_schema; - SyncAppMetadata::Schema m_app_metadata_schema; - - std::shared_ptr get_realm() const; - std::shared_ptr try_get_realm() const; - std::shared_ptr open_realm(bool should_encrypt, bool caller_supplied_key); - - - util::Optional m_app_metadata; -}; - -} // namespace realm - -#endif // REALM_OS_SYNC_METADATA_HPP diff --git a/src/realm/object-store/sync/mongo_client.hpp b/src/realm/object-store/sync/mongo_client.hpp index 7de73b7ca59..9cb419c59d5 100644 --- a/src/realm/object-store/sync/mongo_client.hpp +++ b/src/realm/object-store/sync/mongo_client.hpp @@ -22,12 +22,10 @@ #include #include -namespace realm { -class SyncUser; - -namespace app { +namespace realm::app { class AppServiceClient; class MongoDatabase; +class User; /// A client responsible for communication with a remote MongoDB database. class MongoClient { @@ -47,21 +45,19 @@ class MongoClient { MongoDatabase db(const std::string& name); private: - friend ::realm::SyncUser; - - MongoClient(std::shared_ptr user, std::shared_ptr service, std::string service_name) + friend class User; + MongoClient(std::shared_ptr user, std::shared_ptr service, std::string service_name) : m_user(std::move(user)) , m_service(std::move(service)) , m_service_name(std::move(service_name)) { } - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; std::string m_service_name; }; -} // namespace app -} // namespace realm +} // namespace realm::app #endif /* mongo_client_hpp */ diff --git a/src/realm/object-store/sync/mongo_collection.cpp b/src/realm/object-store/sync/mongo_collection.cpp index ea22e824b0a..5cdd6f8d457 100644 --- a/src/realm/object-store/sync/mongo_collection.cpp +++ b/src/realm/object-store/sync/mongo_collection.cpp @@ -101,8 +101,8 @@ ResponseHandler> get_document_handler(ResponseHandler& user, - const std::shared_ptr& service, const std::string& service_name) + const std::shared_ptr& user, const std::shared_ptr& service, + const std::string& service_name) : m_name(name) , m_database_name(database_name) , m_base_operation_args({{"database", m_database_name}, {"collection", m_name}}) diff --git a/src/realm/object-store/sync/mongo_collection.hpp b/src/realm/object-store/sync/mongo_collection.hpp index bdf08f15628..9b35d1e5ca3 100644 --- a/src/realm/object-store/sync/mongo_collection.hpp +++ b/src/realm/object-store/sync/mongo_collection.hpp @@ -27,10 +27,10 @@ #include namespace realm { -class SyncUser; namespace app { class AppServiceClient; +class User; struct AppError; class MongoCollection { @@ -346,7 +346,7 @@ class MongoCollection { private: friend class MongoDatabase; - MongoCollection(const std::string& name, const std::string& database_name, const std::shared_ptr& user, + MongoCollection(const std::string& name, const std::string& database_name, const std::shared_ptr& user, const std::shared_ptr& service, const std::string& service_name); void call_function(const char* name, const bson::BsonDocument& arg, @@ -361,7 +361,7 @@ class MongoCollection { /// Returns a document of database name and collection name bson::BsonDocument m_base_operation_args; - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; diff --git a/src/realm/object-store/sync/mongo_database.cpp b/src/realm/object-store/sync/mongo_database.cpp index 14176d64d47..18e9cfb287d 100644 --- a/src/realm/object-store/sync/mongo_database.cpp +++ b/src/realm/object-store/sync/mongo_database.cpp @@ -19,8 +19,7 @@ #include #include -namespace realm { -namespace app { +namespace realm::app { MongoCollection MongoDatabase::collection(const std::string& collection_name) { @@ -32,5 +31,4 @@ MongoCollection MongoDatabase::operator[](const std::string& collection_name) return MongoCollection(collection_name, m_name, m_user, m_service, m_service_name); } -} // namespace app -} // namespace realm +} // namespace realm::app diff --git a/src/realm/object-store/sync/mongo_database.hpp b/src/realm/object-store/sync/mongo_database.hpp index 200c71f61ba..255aacacfa4 100644 --- a/src/realm/object-store/sync/mongo_database.hpp +++ b/src/realm/object-store/sync/mongo_database.hpp @@ -22,12 +22,10 @@ #include #include -namespace realm { -class SyncUser; -namespace app { - +namespace realm::app { class AppServiceClient; class MongoCollection; +class User; class MongoDatabase { public: @@ -54,7 +52,7 @@ class MongoDatabase { MongoCollection operator[](const std::string& collection_name); private: - MongoDatabase(std::string name, std::shared_ptr user, std::shared_ptr service, + MongoDatabase(std::string name, std::shared_ptr user, std::shared_ptr service, std::string service_name) : m_name(std::move(name)) , m_user(std::move(user)) @@ -66,12 +64,11 @@ class MongoDatabase { friend class MongoClient; std::string m_name; - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; std::string m_service_name; }; -} // namespace app -} // namespace realm +} // namespace realm::app #endif /* REALM_OS_MONGO_DATABASE_HPP */ diff --git a/src/realm/object-store/sync/push_client.cpp b/src/realm/object-store/sync/push_client.cpp index c87c1160fb6..86c92271305 100644 --- a/src/realm/object-store/sync/push_client.cpp +++ b/src/realm/object-store/sync/push_client.cpp @@ -37,7 +37,7 @@ wrap_completion(util::UniqueFunction)>&& completio } } // anonymous namespace -void PushClient::register_device(const std::string& registration_token, const std::shared_ptr& sync_user, +void PushClient::register_device(const std::string& registration_token, const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion) { auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); @@ -49,7 +49,7 @@ void PushClient::register_device(const std::string& registration_token, const st wrap_completion(std::move(completion))); } -void PushClient::deregister_device(const std::shared_ptr& sync_user, +void PushClient::deregister_device(const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion) { auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); diff --git a/src/realm/object-store/sync/push_client.hpp b/src/realm/object-store/sync/push_client.hpp index 2a904255172..a933752a7d7 100644 --- a/src/realm/object-store/sync/push_client.hpp +++ b/src/realm/object-store/sync/push_client.hpp @@ -26,9 +26,9 @@ #include namespace realm { -class SyncUser; namespace app { class AuthRequestClient; +class User; struct AppError; class PushClient { @@ -53,7 +53,7 @@ class PushClient { /// @param registration_token GCM registration token for the device. /// @param sync_user The sync user requesting push registration. /// @param completion An error will be returned should something go wrong. - void register_device(const std::string& registration_token, const std::shared_ptr& sync_user, + void register_device(const std::string& registration_token, const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion); @@ -61,7 +61,7 @@ class PushClient { /// as it is linked to the user in MongoDB Realm Cloud. /// @param sync_user The sync user requesting push degistration. /// @param completion An error will be returned should something go wrong. - void deregister_device(const std::shared_ptr& sync_user, + void deregister_device(const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion); private: diff --git a/src/realm/object-store/sync/subscribable.hpp b/src/realm/object-store/sync/subscribable.hpp index ff3d9b9b462..84b1e5c58d8 100644 --- a/src/realm/object-store/sync/subscribable.hpp +++ b/src/realm/object-store/sync/subscribable.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include diff --git a/src/realm/object-store/sync/sync_manager.cpp b/src/realm/object-store/sync/sync_manager.cpp index dd1cf9bb712..d64b8cdad22 100644 --- a/src/realm/object-store/sync/sync_manager.cpp +++ b/src/realm/object-store/sync/sync_manager.cpp @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include #include @@ -43,165 +43,16 @@ SyncClientTimeouts::SyncClientTimeouts() { } -SyncManager::SyncManager() = default; - -void SyncManager::configure(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config) +SyncManager::SyncManager(const SyncClientConfig& config) + : m_config(config) { - std::vector> users_to_add; - { - // Locking the mutex here ensures that it is released before locking m_user_mutex - util::CheckedLockGuard lock(m_mutex); - m_app = app; - m_sync_route = sync_route; - m_config = std::move(config); - if (m_sync_client) - return; - - // create a new logger - if the logger_factory is updated later, a new - // logger will be created at that time. - do_make_logger(); - - { - util::CheckedLockGuard lock(m_file_system_mutex); - - // Set up the file manager. - if (m_file_manager) { - // Changing the base path for tests requires calling reset_for_testing() - // first, and otherwise isn't supported - REALM_ASSERT(m_file_manager->base_path() == m_config.base_file_path); - } - else { - m_file_manager = std::make_unique(m_config.base_file_path, app->config().app_id); - } - - // Set up the metadata manager, and perform initial loading/purging work. - if (m_metadata_manager || m_config.metadata_mode == MetadataMode::NoMetadata) { - return; - } - - bool encrypt = m_config.metadata_mode == MetadataMode::Encryption; - m_metadata_manager = std::make_unique(m_file_manager->metadata_path(), encrypt, - m_config.custom_encryption_key); - - REALM_ASSERT(m_metadata_manager); - - // Perform our "on next startup" actions such as deleting Realm files - // which we couldn't delete immediately due to them being in use - std::vector completed_actions; - SyncFileActionMetadataResults file_actions = m_metadata_manager->all_pending_actions(); - for (size_t i = 0; i < file_actions.size(); i++) { - auto file_action = file_actions.get(i); - if (run_file_action(file_action)) { - completed_actions.emplace_back(std::move(file_action)); - } - } - for (auto& action : completed_actions) { - action.remove(); - } - - // Load persisted users into the users map. - SyncUserMetadataResults users = m_metadata_manager->all_unmarked_users(); - for (size_t i = 0; i < users.size(); i++) { - auto user_data = users.get(i); - auto refresh_token = user_data.refresh_token(); - auto access_token = user_data.access_token(); - if (!refresh_token.empty() && !access_token.empty()) { - users_to_add.push_back(std::make_shared(SyncUser::Private(), user_data, this)); - } - } - - // Delete any users marked for death. - std::vector dead_users; - SyncUserMetadataResults users_to_remove = m_metadata_manager->all_users_marked_for_removal(); - dead_users.reserve(users_to_remove.size()); - for (size_t i = 0; i < users_to_remove.size(); i++) { - auto user = users_to_remove.get(i); - // FIXME: delete user data in a different way? (This deletes a logged-out user's data as soon as the - // app launches again, which might not be how some apps want to treat their data.) - try { - m_file_manager->remove_user_realms(user.identity(), user.realm_file_paths()); - dead_users.emplace_back(std::move(user)); - } - catch (FileAccessError const&) { - continue; - } - } - for (auto& user : dead_users) { - user.remove(); - } - } - } - { - util::CheckedLockGuard lock(m_user_mutex); - m_users.insert(m_users.end(), users_to_add.begin(), users_to_add.end()); - } -} - -bool SyncManager::immediately_run_file_actions(const std::string& realm_path) -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (!m_metadata_manager) { - return false; - } - if (auto metadata = m_metadata_manager->get_file_action_metadata(realm_path)) { - if (run_file_action(*metadata)) { - metadata->remove(); - return true; - } - } - return false; -} - -// Perform a file action. Returns whether or not the file action can be removed. -bool SyncManager::run_file_action(SyncFileActionMetadata& md) -{ - switch (md.action()) { - case SyncFileActionMetadata::Action::DeleteRealm: - // Delete all the files for the given Realm. - return m_file_manager->remove_realm(md.original_name()); - case SyncFileActionMetadata::Action::BackUpThenDeleteRealm: - // Copy the primary Realm file to the recovery dir, and then delete the Realm. - auto new_name = md.new_name(); - auto original_name = md.original_name(); - if (!util::File::exists(original_name)) { - // The Realm file doesn't exist anymore. - return true; - } - if (new_name && !util::File::exists(*new_name) && - m_file_manager->copy_realm_file(original_name, *new_name)) { - // We successfully copied the Realm file to the recovery directory. - bool did_remove = m_file_manager->remove_realm(original_name); - // if the copy succeeded but not the delete, then running BackupThenDelete - // a second time would fail, so change this action to just delete the original file. - if (did_remove) { - return true; - } - md.set_action(SyncFileActionMetadata::Action::DeleteRealm); - return false; - } - return false; - } - return false; + // create a new logger - if the logger_factory is updated later, a new + // logger will be created at that time. + do_make_logger(); } void SyncManager::reset_for_testing() { - { - util::CheckedLockGuard lock(m_file_system_mutex); - m_metadata_manager = nullptr; - } - - { - // Destroy all the users. - util::CheckedLockGuard lock(m_user_mutex); - for (auto& user : m_users) { - user->detach_from_sync_manager(); - } - m_users.clear(); - m_current_user = nullptr; - } - { util::CheckedLockGuard lock(m_mutex); // Stop the client. This will abort any uploads that inactive sessions are waiting for. @@ -250,14 +101,7 @@ void SyncManager::reset_for_testing() // Reset even more state. m_config = {}; m_logger_ptr.reset(); - m_sync_route.reset(); - } - - { - util::CheckedLockGuard lock(m_file_system_mutex); - if (m_file_manager) - util::try_remove_dir_recursive(m_file_manager->base_path()); - m_file_manager = nullptr; + m_sync_route.clear(); } } @@ -326,169 +170,6 @@ util::Logger::Level SyncManager::log_level() const noexcept return m_config.log_level; } -bool SyncManager::perform_metadata_update(util::FunctionRef update_function) const -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (!m_metadata_manager) { - return false; - } - update_function(*m_metadata_manager); - return true; -} - -std::shared_ptr SyncManager::get_user(const std::string& user_id, const std::string& refresh_token, - const std::string& access_token, const std::string& device_id) -{ - std::shared_ptr user; - { - util::CheckedLockGuard lock(m_user_mutex); - auto it = std::find_if(m_users.begin(), m_users.end(), [&](const auto& user) { - return user->identity() == user_id && user->state() != SyncUser::State::Removed; - }); - if (it == m_users.end()) { - // No existing user. - auto new_user = std::make_shared(SyncUser::Private(), refresh_token, user_id, access_token, - device_id, this); - m_users.emplace(m_users.begin(), new_user); - { - util::CheckedLockGuard lock(m_file_system_mutex); - // m_current_user is normally set very indirectly via the metadata manger - if (!m_metadata_manager) - m_current_user = new_user; - } - return new_user; - } - - // LoggedOut => LoggedIn - user = *it; - REALM_ASSERT(user->state() != SyncUser::State::Removed); - } - user->log_in(access_token, refresh_token); - return user; -} - -std::vector> SyncManager::all_users() -{ - util::CheckedLockGuard lock(m_user_mutex); - m_users.erase(std::remove_if(m_users.begin(), m_users.end(), - [](auto& user) { - bool should_remove = (user->state() == SyncUser::State::Removed); - if (should_remove) { - user->detach_from_sync_manager(); - } - return should_remove; - }), - m_users.end()); - return m_users; -} - -std::shared_ptr SyncManager::get_user_for_identity(std::string const& identity) const noexcept -{ - auto is_active_user = [identity](auto& el) { - return el->identity() == identity; - }; - auto it = std::find_if(m_users.begin(), m_users.end(), is_active_user); - return it == m_users.end() ? nullptr : *it; -} - -std::shared_ptr SyncManager::get_current_user() const -{ - util::CheckedLockGuard lock(m_user_mutex); - - if (m_current_user) - return m_current_user; - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (!m_metadata_manager) - return nullptr; - - auto cur_user_ident = m_metadata_manager->get_current_user_identity(); - return cur_user_ident ? get_user_for_identity(*cur_user_ident) : nullptr; -} - -void SyncManager::log_out_user(const SyncUser& user) -{ - util::CheckedLockGuard lock(m_user_mutex); - - // Move this user to the end of the vector - auto user_pos = std::partition(m_users.begin(), m_users.end(), [&](auto& u) { - return u.get() != &user; - }); - - auto active_user = std::find_if(m_users.begin(), user_pos, [](auto& u) { - return u->state() == SyncUser::State::LoggedIn; - }); - - util::CheckedLockGuard fs_lock(m_file_system_mutex); - bool was_active = m_current_user.get() == &user || - (m_metadata_manager && m_metadata_manager->get_current_user_identity() == user.identity()); - if (!was_active) - return; - - // Set the current active user to the next logged in user, or null if none - if (active_user != user_pos) { - m_current_user = *active_user; - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity((*active_user)->identity()); - } - else { - m_current_user = nullptr; - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(""); - } -} - -void SyncManager::set_current_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - - m_current_user = get_user_for_identity(user_id); - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(user_id); -} - -void SyncManager::remove_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - if (auto user = get_user_for_identity(user_id)) - user->invalidate(); -} - -void SyncManager::delete_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - // Avoid iterating over m_users twice by not calling `get_user_for_identity`. - auto it = std::find_if(m_users.begin(), m_users.end(), [&user_id](auto& user) { - return user->identity() == user_id; - }); - auto user = it == m_users.end() ? nullptr : *it; - - if (!user) - return; - - // Deletion should happen immediately, not when we do the cleanup - // task on next launch. - m_users.erase(it); - user->detach_from_sync_manager(); - - if (m_current_user && m_current_user->identity() == user->identity()) - m_current_user = nullptr; - - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (!m_metadata_manager) - return; - - auto users = m_metadata_manager->all_unmarked_users(); - for (size_t i = 0; i < users.size(); i++) { - auto metadata = users.get(i); - if (user->identity() == metadata.identity()) { - m_file_manager->remove_user_realms(metadata.identity(), metadata.realm_file_paths()); - metadata.remove(); - break; - } - } -} - SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS { // Grab the current sessions under a lock so we can shut them down. We have to @@ -504,13 +185,6 @@ SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS session->detach_from_sync_manager(); } - { - util::CheckedLockGuard lk(m_user_mutex); - for (auto& user : m_users) { - user->detach_from_sync_manager(); - } - } - { util::CheckedLockGuard lk(m_mutex); // Stop the client. This will abort any uploads that inactive sessions are waiting for. @@ -519,89 +193,26 @@ SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS } } -std::shared_ptr SyncManager::get_existing_logged_in_user(const std::string& user_id) const -{ - util::CheckedLockGuard lock(m_user_mutex); - auto user = get_user_for_identity(user_id); - return user && user->state() == SyncUser::State::LoggedIn ? user : nullptr; -} - -struct UnsupportedBsonPartition : public std::logic_error { - UnsupportedBsonPartition(std::string msg) - : std::logic_error(msg) - { - } -}; - -static std::string string_from_partition(const std::string& partition) -{ - bson::Bson partition_value = bson::parse(partition); - switch (partition_value.type()) { - case bson::Bson::Type::Int32: - return util::format("i_%1", static_cast(partition_value)); - case bson::Bson::Type::Int64: - return util::format("l_%1", static_cast(partition_value)); - case bson::Bson::Type::String: - return util::format("s_%1", static_cast(partition_value)); - case bson::Bson::Type::ObjectId: - return util::format("o_%1", static_cast(partition_value).to_string()); - case bson::Bson::Type::Uuid: - return util::format("u_%1", static_cast(partition_value).to_string()); - case bson::Bson::Type::Null: - return "null"; - default: - throw UnsupportedBsonPartition(util::format("Unsupported partition key value: '%1'. Only int, string " - "UUID and ObjectId types are currently supported.", - partition_value.to_string())); - } -} - -std::string SyncManager::path_for_realm(const SyncConfig& config, util::Optional custom_file_name) const +std::vector> SyncManager::get_all_sessions() const { - auto user = config.user; - REALM_ASSERT(user); - std::string path; - { - util::CheckedLockGuard lock(m_file_system_mutex); - REALM_ASSERT(m_file_manager); - - // Attempt to make a nicer filename which will ease debugging when - // locating files in the filesystem. - auto file_name = [&]() -> std::string { - if (custom_file_name) { - return *custom_file_name; - } - if (config.flx_sync_requested) { - REALM_ASSERT_DEBUG(config.partition_value.empty()); - return "flx_sync_default"; - } - return string_from_partition(config.partition_value); - }(); - path = m_file_manager->realm_file_path(user->identity(), user->legacy_identities(), file_name, - config.partition_value); + util::CheckedLockGuard lock(m_session_mutex); + std::vector> sessions; + for (auto& [_, session] : m_sessions) { + if (auto external_reference = session->existing_external_reference()) + sessions.push_back(std::move(external_reference)); } - // Report the use of a Realm for this user, so the metadata can track it for clean up. - perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(user->identity()); - metadata->add_realm_file_path(path); - }); - return path; -} - -std::string SyncManager::recovery_directory_path(util::Optional const& custom_dir_name) const -{ - util::CheckedLockGuard lock(m_file_system_mutex); - REALM_ASSERT(m_file_manager); - return m_file_manager->recovery_directory_path(custom_dir_name); + return sessions; } -std::vector> SyncManager::get_all_sessions() const +std::vector> SyncManager::get_all_sessions_for(const SyncUser& user) const { util::CheckedLockGuard lock(m_session_mutex); std::vector> sessions; for (auto& [_, session] : m_sessions) { - if (auto external_reference = session->existing_external_reference()) - sessions.push_back(std::move(external_reference)); + if (session->user().get() == &user) { + if (auto external_reference = session->existing_external_reference()) + sessions.push_back(std::move(external_reference)); + } } return sessions; } @@ -644,7 +255,6 @@ std::shared_ptr SyncManager::get_session(std::shared_ptr db, co util::CheckedUniqueLock lock(m_session_mutex); if (auto session = get_existing_session_locked(path)) { - config.sync_config->user->register_session(session); return session->external_reference(); } @@ -653,15 +263,7 @@ std::shared_ptr SyncManager::get_session(std::shared_ptr db, co // Create the external reference immediately to ensure that the session will become // inactive if an exception is thrown in the following code. - auto external_reference = shared_session->external_reference(); - // unlocking m_session_mutex here prevents a deadlock for synchronous network - // transports such as the unit test suite, in the case where the log in request is - // denied by the server: Active -> WaitingForAccessToken -> handle_refresh(401 - // error) -> user.log_out() -> unregister_session (locks m_session_mutex again) - lock.unlock(); - config.sync_config->user->register_session(std::move(shared_session)); - - return external_reference; + return shared_session->external_reference(); } bool SyncManager::has_existing_sessions() @@ -749,15 +351,6 @@ std::unique_ptr SyncManager::create_sync_client() const return std::make_unique(m_logger_ptr, m_config, weak_from_this()); } -util::Optional SyncManager::app_metadata() const -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (!m_metadata_manager) { - return util::none; - } - return m_metadata_manager->get_app_metadata(); -} - void SyncManager::close_all_sessions() { // log_out() will call unregister_session(), which requires m_session_mutex, diff --git a/src/realm/object-store/sync/sync_manager.hpp b/src/realm/object-store/sync/sync_manager.hpp index 782c8790252..343ac72f9fd 100644 --- a/src/realm/object-store/sync/sync_manager.hpp +++ b/src/realm/object-store/sync/sync_manager.hpp @@ -19,17 +19,9 @@ #ifndef REALM_OS_SYNC_MANAGER_HPP #define REALM_OS_SYNC_MANAGER_HPP -#include - +#include #include -#include -#include -#include -#include -#include - -#include -#include + #include class TestAppSession; @@ -39,86 +31,21 @@ namespace realm { class DB; struct SyncConfig; +struct RealmConfig; class SyncSession; class SyncUser; class SyncFileManager; -class SyncMetadataManager; -class SyncFileActionMetadata; -class SyncAppMetadata; namespace _impl { struct SyncClient; } -namespace app { -class App; -} - -struct SyncClientTimeouts { - SyncClientTimeouts(); - // See sync::Client::Config for the meaning of these fields. - uint64_t connect_timeout; - uint64_t connection_linger_time; - uint64_t ping_keepalive_period; - uint64_t pong_keepalive_timeout; - uint64_t fast_reconnect_limit; -}; - -struct SyncClientConfig { - enum class MetadataMode { - NoEncryption, // Enable metadata, but disable encryption. - Encryption, // Enable metadata, and use encryption (automatic if possible). - NoMetadata, // Disable metadata. - }; - - std::string base_file_path; - MetadataMode metadata_mode = MetadataMode::Encryption; - util::Optional> custom_encryption_key; - - using LoggerFactory = std::function(util::Logger::Level)>; - LoggerFactory logger_factory; - util::Logger::Level log_level = util::Logger::Level::info; - ReconnectMode reconnect_mode = ReconnectMode::normal; // For internal sync-client testing only! -#if REALM_DISABLE_SYNC_MULTIPLEXING - bool multiplex_sessions = false; -#else - bool multiplex_sessions = true; -#endif - - // The SyncSocket instance used by the Sync Client for event synchronization - // and creating WebSockets. If not provided the default implementation will be used. - std::shared_ptr socket_provider; - - // Optional thread observer for event loop thread events in the default SyncSocketProvider - // implementation. It is not used for custom SyncSocketProvider implementations. - std::shared_ptr default_socket_provider_thread_observer; - - // {@ - // Optional information about the binding/application that is sent as part of the User-Agent - // when establishing a connection to the server. These values are only used by the default - // SyncSocket implementation. Custom SyncSocket implementations must update the User-Agent - // directly, if supported by the platform APIs. - std::string user_agent_binding_info; - std::string user_agent_application_info; - // @} - - SyncClientTimeouts timeouts; -}; - -class SyncManager : public std::enable_shared_from_this { +class SyncManager : private std::enable_shared_from_this { friend class SyncSession; friend class ::TestSyncManager; friend class ::TestAppSession; public: - using MetadataMode = SyncClientConfig::MetadataMode; - - // Immediately run file actions for a single Realm at a given original path. - // Returns whether or not a file action was successfully executed for the specified Realm. - // Preconditions: all references to the Realm at the given path must have already been invalidated. - // The metadata and file management subsystems must also have already been configured. - bool immediately_run_file_actions(const std::string& original_name) REQUIRES(!m_file_system_mutex); - // Enables/disables using a single connection for all sync sessions for each host/port/user rather // than one per session. // This must be called before any sync sessions are created, cannot be @@ -157,6 +84,8 @@ class SyncManager : public std::enable_shared_from_this { util::Logger::Level log_level() const noexcept REQUIRES(!m_mutex); std::vector> get_all_sessions() const REQUIRES(!m_session_mutex); + std::vector> get_all_sessions_for(const SyncUser& user) const + REQUIRES(!m_session_mutex); std::shared_ptr get_session(std::shared_ptr db, const RealmConfig& config) REQUIRES(!m_mutex, !m_session_mutex); std::shared_ptr get_existing_session(const std::string& path) const REQUIRES(!m_session_mutex); @@ -174,55 +103,10 @@ class SyncManager : public std::enable_shared_from_this { // makes it possible to guarantee that all sessions have, in fact, been closed. void wait_for_sessions_to_terminate() REQUIRES(!m_mutex); - // If the metadata manager is configured, perform an update. Returns `true` if the code was run. - bool perform_metadata_update(util::FunctionRef update_function) const - REQUIRES(!m_file_system_mutex); - - // Get a sync user for a given identity, or create one if none exists yet, and set its token. - // If a logged-out user exists, it will marked as logged back in. - std::shared_ptr get_user(const std::string& user_id, const std::string& refresh_token, - const std::string& access_token, const std::string& device_id) - REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Get an existing user for a given identifier, if one exists and is logged in. - std::shared_ptr get_existing_logged_in_user(const std::string& user_id) const REQUIRES(!m_user_mutex); - - // Get all the users that are logged in and not errored out. - std::vector> all_users() REQUIRES(!m_user_mutex); - - // Gets the currently active user. - std::shared_ptr get_current_user() const REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Log out a given user - void log_out_user(const SyncUser& user) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Sets the currently active user. - void set_current_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Removes a user - void remove_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Permanently deletes a user. - void delete_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Get the default path for a Realm for the given configuration. - // The default value is `///.realm`. - // If the file cannot be created at this location, for example due to path length restrictions, - // this function may pass back `/.realm` - std::string path_for_realm(const SyncConfig& config, util::Optional custom_file_name = none) const - REQUIRES(!m_file_system_mutex); - - // Get the path of the recovery directory for backed-up or recovered Realms. - std::string recovery_directory_path(util::Optional const& custom_dir_name = none) const - REQUIRES(!m_file_system_mutex); - // Reset the singleton state for testing purposes. DO NOT CALL OUTSIDE OF TESTING CODE. // Precondition: any synced Realms or `SyncSession`s must be closed or rendered inactive prior to // calling this method. - void reset_for_testing() REQUIRES(!m_mutex, !m_file_system_mutex, !m_user_mutex, !m_session_mutex); - - // Get the app metadata for the active app. - util::Optional app_metadata() const REQUIRES(!m_file_system_mutex); + void reset_for_testing() REQUIRES(!m_mutex, !m_session_mutex); // Immediately closes any open sync sessions for this sync manager void close_all_sessions() REQUIRES(!m_mutex, !m_session_mutex); @@ -246,12 +130,6 @@ class SyncManager : public std::enable_shared_from_this { return m_sync_route; } - std::weak_ptr app() const REQUIRES(!m_mutex) - { - util::CheckedLockGuard lock(m_mutex); - return m_app; - } - SyncClientConfig config() const REQUIRES(!m_mutex) { util::CheckedLockGuard lock(m_mutex); @@ -261,9 +139,7 @@ class SyncManager : public std::enable_shared_from_this { // Return the cached logger const std::shared_ptr& get_logger() const REQUIRES(!m_mutex); - SyncManager(); - SyncManager(const SyncManager&) = delete; - SyncManager& operator=(const SyncManager&) = delete; + SyncManager(const SyncClientConfig& config); struct OnlyForTesting { friend class TestHelper; @@ -271,19 +147,18 @@ class SyncManager : public std::enable_shared_from_this { static void voluntary_disconnect_all_connections(SyncManager&); }; -protected: - friend class SyncUser; - friend class SyncSesson; - - using std::enable_shared_from_this::shared_from_this; - using std::enable_shared_from_this::weak_from_this; - private: - friend class app::App; + util::CheckedMutex m_mutex; + mutable std::unique_ptr<_impl::SyncClient> m_sync_client GUARDED_BY(m_mutex); + SyncClientConfig m_config GUARDED_BY(m_mutex); + std::shared_ptr m_logger_ptr GUARDED_BY(m_mutex); + std::string m_sync_route GUARDED_BY(m_mutex); - void configure(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config) - REQUIRES(!m_mutex, !m_file_system_mutex, !m_user_mutex, !m_session_mutex); + // Map of sessions by path name. + // Sessions remove themselves from this map by calling `unregister_session` once they're + // inactive and have performed any necessary cleanup work. + util::CheckedMutex m_session_mutex; + std::unordered_map> m_sessions GUARDED_BY(m_session_mutex); // Stop tracking the session for the given path if it is inactive. // No-op if the session is either still active or in the active sessions list @@ -295,51 +170,14 @@ class SyncManager : public std::enable_shared_from_this { std::shared_ptr get_existing_session_locked(const std::string& path) const REQUIRES(m_session_mutex); - std::shared_ptr get_user_for_identity(std::string const& identity) const noexcept - REQUIRES(m_user_mutex); - - mutable util::CheckedMutex m_mutex; - - bool run_file_action(SyncFileActionMetadata&) REQUIRES(m_file_system_mutex); void init_metadata(SyncClientConfig config, const std::string& app_id); // internally create a new logger - used by configure() and set_logger_factory() void do_make_logger() REQUIRES(m_mutex); - // Protects m_users - mutable util::CheckedMutex m_user_mutex; - - // A vector of all SyncUser objects. - std::vector> m_users GUARDED_BY(m_user_mutex); - std::shared_ptr m_current_user GUARDED_BY(m_user_mutex); - - mutable std::unique_ptr<_impl::SyncClient> m_sync_client GUARDED_BY(m_mutex); - - SyncClientConfig m_config GUARDED_BY(m_mutex); - mutable std::shared_ptr m_logger_ptr GUARDED_BY(m_mutex); - - // Protects m_file_manager and m_metadata_manager - mutable util::CheckedMutex m_file_system_mutex; - std::unique_ptr m_file_manager GUARDED_BY(m_file_system_mutex); - std::unique_ptr m_metadata_manager GUARDED_BY(m_file_system_mutex); - - // Protects m_sessions - mutable util::CheckedMutex m_session_mutex; - - // Map of sessions by path name. - // Sessions remove themselves from this map by calling `unregister_session` once they're - // inactive and have performed any necessary cleanup work. - std::unordered_map> m_sessions GUARDED_BY(m_session_mutex); - // Internal method returning `true` if the SyncManager still contains sessions not yet fully closed. // Callers of this method should hold the `m_session_mutex` themselves. bool do_has_existing_sessions() REQUIRES(m_session_mutex); - - // The sync route URL to connect to the server. This can be initially empty, but should not - // be cleared once it has been set to a value, except by `reset_for_testing()`. - std::optional m_sync_route GUARDED_BY(m_mutex); - - std::weak_ptr m_app GUARDED_BY(m_mutex); }; } // namespace realm diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index 676379a32aa..8b72ca53fac 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -269,7 +269,7 @@ void SyncSession::handle_bad_auth(const std::shared_ptr& user, Status cancel_pending_waits(std::move(lock), status); } if (user) { - user->log_out(); + user->request_log_out(nullptr); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -425,11 +425,10 @@ SyncSession::SyncSession(Private, SyncClient& client, std::shared_ptr db, co } } -std::shared_ptr SyncSession::sync_manager() const +SyncManager* SyncSession::sync_manager() const { util::CheckedLockGuard lk(m_state_mutex); - REALM_ASSERT(m_sync_manager); - return m_sync_manager->shared_from_this(); + return m_sync_manager; } void SyncSession::detach_from_sync_manager() @@ -441,6 +440,7 @@ void SyncSession::detach_from_sync_manager() void SyncSession::update_error_and_mark_file_for_deletion(SyncError& error, ShouldBackup should_backup) { + SyncFileManager* file_manager = nullptr; // FIXME: get a file manager from somewhere? util::CheckedLockGuard config_lock(m_config_mutex); // Add a SyncFileActionMetadata marking the Realm as needing to be deleted. std::string recovery_path; @@ -448,18 +448,14 @@ void SyncSession::update_error_and_mark_file_for_deletion(SyncError& error, Shou error.user_info[SyncError::c_original_file_path_key] = original_path; if (should_backup == ShouldBackup::yes) { recovery_path = util::reserve_unique_file_name( - m_sync_manager->recovery_directory_path(m_config.sync_config->recovery_directory), + file_manager->recovery_directory_path(m_config.sync_config->recovery_directory), util::create_timestamped_template("recovered_realm")); error.user_info[SyncError::c_recovery_file_path_key] = recovery_path; } - using Action = SyncFileActionMetadata::Action; + using Action = SyncFileAction; auto action = should_backup == ShouldBackup::yes ? Action::BackUpThenDeleteRealm : Action::DeleteRealm; - m_sync_manager->perform_metadata_update([action, original_path = std::move(original_path), - recovery_path = std::move(recovery_path), - partition_value = m_config.sync_config->partition_value, - identity = m_config.sync_config->user->identity()](const auto& manager) { - manager.make_file_action_metadata(original_path, partition_value, identity, action, recovery_path); - }); + m_config.sync_config->user->create_file_action(action, original_path, recovery_path, + m_config.sync_config->partition_value); } void SyncSession::download_fresh_realm(sync::ProtocolErrorInfo::Action server_requests_action) @@ -764,13 +760,13 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) return; case sync::ProtocolErrorInfo::Action::RefreshUser: if (auto u = user()) { - u->refresh_custom_data(false, handle_refresh(shared_from_this(), false)); + u->request_refresh_user(handle_refresh(shared_from_this(), false)); return; } break; case sync::ProtocolErrorInfo::Action::RefreshLocation: if (auto u = user()) { - u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); + u->request_refresh_location(handle_refresh(shared_from_this(), true)); return; } break; @@ -829,7 +825,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) if (log_out_user) { if (auto u = user()) - u->log_out(); + u->request_log_out(nullptr); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -934,7 +930,7 @@ void SyncSession::create_sync_session() sync::Session::Config session_config; session_config.signed_user_token = sync_config.user->access_token(); - session_config.user_id = sync_config.user->identity(); + session_config.user_id = sync_config.user->user_id(); session_config.realm_identifier = sync_config.partition_value; session_config.verify_servers_ssl_certificate = sync_config.client_validate_ssl; session_config.ssl_trust_certificate_path = sync_config.ssl_trust_certificate_path; @@ -1233,7 +1229,7 @@ void SyncSession::update_access_token(const std::string& signed_token) void SyncSession::initiate_access_token_refresh() { if (auto session_user = user()) { - session_user->refresh_custom_data(handle_refresh(shared_from_this(), false)); + session_user->request_access_token(handle_refresh(shared_from_this(), false)); } } diff --git a/src/realm/object-store/sync/sync_session.hpp b/src/realm/object-store/sync/sync_session.hpp index 5c042edc281..124b07cde54 100644 --- a/src/realm/object-store/sync/sync_session.hpp +++ b/src/realm/object-store/sync/sync_session.hpp @@ -360,11 +360,11 @@ class SyncSession : public std::enable_shared_from_this { const RealmConfig& config, SyncManager* sync_manager) { REALM_ASSERT(config.sync_config); - return std::make_shared(Private(), client, std::move(db), config, std::move(sync_manager)); + return std::make_shared(Private(), client, std::move(db), config, sync_manager); } // } - std::shared_ptr sync_manager() const REQUIRES(!m_state_mutex); + SyncManager* sync_manager() const REQUIRES(!m_state_mutex); static util::UniqueFunction)> handle_refresh(const std::shared_ptr&, bool); diff --git a/src/realm/object-store/sync/sync_user.cpp b/src/realm/object-store/sync/sync_user.cpp index 2dc130d8a6a..8896f8e68c2 100644 --- a/src/realm/object-store/sync/sync_user.cpp +++ b/src/realm/object-store/sync/sync_user.cpp @@ -18,18 +18,16 @@ #include -#include -#include -#include -#include -#include #include #include +#include #include namespace realm { +UserProvider::~UserProvider() = default; + static std::string base64_decode(const std::string& in) { std::string out; @@ -59,7 +57,7 @@ static std::vector split_token(const std::string& jwt) return parts; } -RealmJWT::RealmJWT(const std::string& token) +RealmJWT::RealmJWT(std::string_view token) : token(token) { auto parts = split_token(this->token); @@ -75,404 +73,180 @@ RealmJWT::RealmJWT(const std::string& token) } } -SyncUserIdentity::SyncUserIdentity(const std::string& id, const std::string& provider_type) - : id(id) - , provider_type(provider_type) +RealmJWT::RealmJWT(StringData token) + : RealmJWT(std::string_view(token)) { } -SyncUser::SyncUser(Private, const std::string& refresh_token, const std::string& id, const std::string& access_token, - const std::string& device_id, SyncManager* sync_manager) - : m_state(State::LoggedIn) - , m_identity(id) - , m_refresh_token(RealmJWT(refresh_token)) - , m_access_token(RealmJWT(access_token)) - , m_device_id(device_id) - , m_sync_manager(sync_manager) +RealmJWT::RealmJWT(const std::string& token) + : RealmJWT(std::string_view(token)) { - REALM_ASSERT(!access_token.empty() && !refresh_token.empty()); - - m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedIn, m_access_token.token, m_refresh_token.token); - metadata->set_device_id(m_device_id); - m_legacy_identities = metadata->legacy_identities(); - this->m_user_profile = metadata->profile(); - }); } -SyncUser::SyncUser(Private, const SyncUserMetadata& data, SyncManager* sync_manager) - : m_state(data.state()) - , m_legacy_identities(data.legacy_identities()) - , m_identity(data.identity()) - , m_refresh_token(RealmJWT(data.refresh_token())) - , m_access_token(RealmJWT(data.access_token())) - , m_user_identities(data.identities()) - , m_user_profile(data.profile()) - , m_device_id(data.device_id()) - , m_sync_manager(sync_manager) -{ - // Check for inconsistent state in the metadata Realm. This shouldn't happen, - // but previous versions could sometimes mark a user as logged in with an - // empty refresh token. - if (m_state == State::LoggedIn && (m_refresh_token.token.empty() || m_access_token.token.empty())) { - m_state = State::LoggedOut; - m_refresh_token = {}; - m_access_token = {}; - } -} -std::shared_ptr SyncUser::sync_manager() const +SyncUser::SyncUser(std::shared_ptr provider, std::shared_ptr manager, + std::string_view app_id, std::string_view user_id) + : m_provider(std::move(provider)) + , m_sync_manager(std::move(manager)) + , m_app_id(app_id) + , m_user_id(user_id) { - util::CheckedLockGuard lk(m_mutex); - if (m_state == State::Removed) { - throw app::AppError( - ErrorCodes::ClientUserNotFound, - util::format("Cannot start a sync session for user '%1' because this user has been removed.", - m_identity)); - } - REALM_ASSERT(m_sync_manager); - return m_sync_manager->shared_from_this(); + provider->register_sync_user(*this); } -void SyncUser::detach_from_sync_manager() +SyncUser::~SyncUser() { - util::CheckedLockGuard lk(m_mutex); - REALM_ASSERT(m_sync_manager); - m_state = SyncUser::State::Removed; - m_sync_manager = nullptr; + if (m_provider) { + m_provider->unregister_sync_user(*this); + } + // syncsession retains user, so there can't be any sessions now } -std::vector> SyncUser::all_sessions() +void SyncUser::detach_and_tear_down() { - util::CheckedLockGuard lock(m_mutex); - std::vector> sessions; - if (m_state == State::Removed) { - return sessions; - } - for (auto it = m_sessions.begin(); it != m_sessions.end();) { - if (auto ptr_to_session = it->second.lock()) { - sessions.emplace_back(std::move(ptr_to_session)); - it++; - continue; + // it feels like there's probably a race condition with this being called + // at the wrong time as a sync session is being established on another thread + std::shared_ptr sync_manager; + { + util::CheckedLockGuard lk(m_mutex); + m_data.access_token.token = ""; + m_data.refresh_token.token = ""; + m_data.state = UserState::Removed; + if (m_provider) { + m_provider->unregister_sync_user(*this); } - // This session is bad, destroy it. - it = m_sessions.erase(it); + m_provider.reset(); + sync_manager.swap(m_sync_manager); } - return sessions; -} -std::shared_ptr SyncUser::session_for_on_disk_path(const std::string& path) -{ - util::CheckedLockGuard lock(m_mutex); - if (m_state == State::Removed) { - return nullptr; - } - auto it = m_sessions.find(path); - if (it == m_sessions.end()) { - return nullptr; - } - auto locked = it->second.lock(); - if (!locked) { - // Remove the session from the map, because it has fatally errored out or the entry is invalid. - m_sessions.erase(it); + if (sync_manager) { + for (auto session : m_sync_manager->get_all_sessions_for(*this)) { + session->force_close(); + } } - return locked; } -void SyncUser::log_in(const std::string& access_token, const std::string& refresh_token) +void SyncUser::update_backing_data(SyncUserData&& data) { - REALM_ASSERT(!access_token.empty()); - REALM_ASSERT(!refresh_token.empty()); - std::vector> sessions_to_revive; - { - util::CheckedLockGuard lock1(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - m_state = State::LoggedIn; - m_access_token = RealmJWT(access_token); - m_refresh_token = RealmJWT(refresh_token); - sessions_to_revive = revive_sessions(); - - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedIn, access_token, refresh_token); - }); - } - // (Re)activate all pending sessions. - // Note that we do this after releasing the lock, since the session may - // need to access protected User state in the process of binding itself. - for (auto& session : sessions_to_revive) { - session->revive_if_needed(); + if (data.state == UserState::Removed) { + detach_and_tear_down(); + return; } - emit_change_to_subscribers(*this); -} - -void SyncUser::invalidate() -{ + bool is_logged_in = data.state == UserState::LoggedIn; + if (is_logged_in) { + REALM_ASSERT(!data.access_token.token.empty()); + REALM_ASSERT(!data.refresh_token.token.empty()); + } + bool was_logged_in; + std::string new_token; { util::CheckedLockGuard lock1(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - m_state = State::Removed; - m_access_token = {}; - m_refresh_token = {}; - - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::Removed, "", ""); - }); + was_logged_in = data.state == UserState::LoggedIn; + if (data.access_token.token != m_data.access_token.token) + new_token = data.access_token.token; + m_data = std::move(data); } - emit_change_to_subscribers(*this); -} -std::vector> SyncUser::revive_sessions() -{ - std::vector> sessions_to_revive; - sessions_to_revive.reserve(m_waiting_sessions.size()); - for (auto& [path, weak_session] : m_waiting_sessions) { - if (auto ptr = weak_session.lock()) { - m_sessions[path] = ptr; - sessions_to_revive.emplace_back(std::move(ptr)); + if (is_logged_in && !was_logged_in) { + for (auto session : m_sync_manager->get_all_sessions_for(*this)) { + session->revive_if_needed(); } } - m_waiting_sessions.clear(); - return sessions_to_revive; -} - -void SyncUser::update_access_token(std::string&& token) -{ - { - util::CheckedLockGuard lock(m_mutex); - if (m_state != State::LoggedIn) - return; - - util::CheckedLockGuard lock2(m_tokens_mutex); - m_access_token = RealmJWT(std::move(token)); - m_sync_manager->perform_metadata_update([&, raw_access_token = m_access_token.token](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_access_token(raw_access_token); - }); - } - - emit_change_to_subscribers(*this); -} - -std::vector SyncUser::identities() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_user_identities; -} - -void SyncUser::log_out() -{ - // We'll extend the lifetime of SyncManager while holding m_mutex so that we know it's safe to call methods on it - // after we've been marked as logged out. - std::shared_ptr sync_manager_shared; - { - util::CheckedLockGuard lock(m_mutex); - bool is_anonymous = false; - { - util::CheckedLockGuard lock2(m_tokens_mutex); - if (m_state != State::LoggedIn) { - return; - } - is_anonymous = do_is_anonymous(); - m_state = State::LoggedOut; - m_access_token = RealmJWT{}; - m_refresh_token = RealmJWT{}; - } - - if (is_anonymous) { - // An Anonymous user can not log back in. - // Mark the user as 'dead' in the persisted metadata Realm. - m_state = State::Removed; - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, false); - if (metadata) - metadata->remove(); - }); - } - else { - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedOut, "", ""); - }); + else if (!is_logged_in && was_logged_in) { + for (auto session : m_sync_manager->get_all_sessions_for(*this)) { + session->force_close(); } - sync_manager_shared = m_sync_manager->shared_from_this(); - // Move all active sessions into the waiting sessions pool. If the user is - // logged back in, they will automatically be reactivated. - for (auto& [path, weak_session] : m_sessions) { - if (auto ptr = weak_session.lock()) { - ptr->force_close(); - m_waiting_sessions[path] = std::move(ptr); - } + } + else if (!new_token.empty()) { + for (auto session : m_sync_manager->get_all_sessions_for(*this)) { + session->update_access_token(new_token); } - m_sessions.clear(); } - - sync_manager_shared->log_out_user(*this); - emit_change_to_subscribers(*this); } bool SyncUser::is_logged_in() const { util::CheckedLockGuard lock(m_mutex); - return m_state == State::LoggedIn; -} - -bool SyncUser::is_anonymous() const -{ - util::CheckedLockGuard lock(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - return do_is_anonymous(); -} - -bool SyncUser::do_is_anonymous() const -{ - return m_state == State::LoggedIn && m_user_identities.size() == 1 && - m_user_identities[0].provider_type == app::IdentityProviderAnonymous; + return m_data.state == UserState::LoggedIn; } std::string SyncUser::refresh_token() const { - util::CheckedLockGuard lock(m_tokens_mutex); - return m_refresh_token.token; + util::CheckedLockGuard lock(m_mutex); + return m_data.refresh_token.token; } std::string SyncUser::access_token() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_access_token.token; -} - -std::string SyncUser::device_id() const { util::CheckedLockGuard lock(m_mutex); - return m_device_id; + return m_data.access_token.token; } -bool SyncUser::has_device_id() const +UserState SyncUser::state() const { util::CheckedLockGuard lock(m_mutex); - return !m_device_id.empty() && m_device_id != "000000000000000000000000"; + return m_data.state; } -SyncUser::State SyncUser::state() const +bool SyncUser::access_token_refresh_required() const { + using namespace std::chrono; + constexpr size_t buffer_seconds = 5; // arbitrary util::CheckedLockGuard lock(m_mutex); - return m_state; + const auto now = duration_cast(system_clock::now().time_since_epoch()).count() + + m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed); + const auto threshold = now - buffer_seconds; + return !m_data.access_token.token.empty() && m_data.access_token.expires_at < static_cast(threshold); } -SyncUserProfile SyncUser::user_profile() const +void SyncUser::request_log_out(util::UniqueFunction)>&& completion) { + // FIXME: probably unsafe to call all these under lock util::CheckedLockGuard lock(m_mutex); - return m_user_profile; -} - -util::Optional SyncUser::custom_data() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_access_token.user_data; + if (m_provider) { + m_provider->request_log_out(m_user_id, std::move(completion)); + } } -void SyncUser::update_user_profile(std::vector identities, SyncUserProfile profile) +void SyncUser::request_refresh_user(util::UniqueFunction)>&& completion) { util::CheckedLockGuard lock(m_mutex); - if (m_state == SyncUser::State::Removed) { - return; + if (m_provider) { + m_provider->request_refresh_user(m_user_id, std::move(completion)); } - - m_user_identities = std::move(identities); - m_user_profile = std::move(profile); - - m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_identities(m_user_identities); - metadata->set_user_profile(m_user_profile); - }); } -void SyncUser::register_session(std::shared_ptr session) +void SyncUser::request_refresh_location(util::UniqueFunction)>&& completion) { - const std::string& path = session->path(); - util::CheckedUniqueLock lock(m_mutex); - switch (m_state) { - case State::LoggedIn: - m_sessions[path] = session; - break; - case State::LoggedOut: - m_waiting_sessions[path] = session; - break; - case State::Removed: - break; + util::CheckedLockGuard lock(m_mutex); + if (m_provider) { + m_provider->request_refresh_location(m_user_id, std::move(completion)); } } -app::MongoClient SyncUser::mongo_client(const std::string& service_name) -{ - util::CheckedLockGuard lk(m_mutex); - REALM_ASSERT(m_state == SyncUser::State::LoggedIn); - return app::MongoClient(shared_from_this(), m_sync_manager->app().lock(), service_name); -} - -void SyncUser::refresh_custom_data(util::UniqueFunction)> completion_block) - REQUIRES(!m_mutex) +void SyncUser::request_access_token(util::UniqueFunction)>&& completion) { - refresh_custom_data(false, std::move(completion_block)); + util::CheckedLockGuard lock(m_mutex); + if (m_provider) { + m_provider->request_access_token(m_user_id, std::move(completion)); + } } -void SyncUser::refresh_custom_data(bool update_location, - util::UniqueFunction)> completion_block) +void SyncUser::track_realm(std::string_view path) { - std::shared_ptr app; - std::shared_ptr user; - { - util::CheckedLockGuard lk(m_mutex); - if (m_state != SyncUser::State::Removed) { - user = shared_from_this(); - } - if (m_sync_manager) { - app = m_sync_manager->app().lock(); - } - } - if (!user) { - completion_block(app::AppError( - ErrorCodes::ClientUserNotFound, - util::format("Cannot initiate a refresh on user '%1' because the user has been removed", m_identity))); - } - else if (!app) { - completion_block(app::AppError( - ErrorCodes::ClientAppDeallocated, - util::format("Cannot initiate a refresh on user '%1' because the app has been deallocated", m_identity))); - } - else { - std::weak_ptr weak_user = user->weak_from_this(); - app->refresh_custom_data(user, update_location, - [completion_block = std::move(completion_block), weak_user](auto error) { - if (auto strong = weak_user.lock()) { - strong->emit_change_to_subscribers(*strong); - } - completion_block(error); - }); + util::CheckedLockGuard lock(m_mutex); + if (m_provider) { + m_provider->realm_opened(m_user_id, path); } } -bool SyncUser::access_token_refresh_required() const +void SyncUser::create_file_action(SyncFileAction action, std::string_view original_path, + std::string_view recovery_path, std::string_view partition_value) { - using namespace std::chrono; - constexpr size_t buffer_seconds = 5; // arbitrary - util::CheckedLockGuard lock(m_tokens_mutex); - const auto now = duration_cast(system_clock::now().time_since_epoch()).count() + - m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed); - const auto threshold = now - buffer_seconds; - return !m_access_token.token.empty() && m_access_token.expires_at < static_cast(threshold); + util::CheckedLockGuard lock(m_mutex); + if (m_provider) { + m_provider->create_file_action(m_user_id, action, original_path, recovery_path, partition_value); + } } - } // namespace realm - -namespace std { -size_t hash::operator()(const realm::SyncUserIdentity& k) const -{ - return ((hash()(k.id) ^ (hash()(k.provider_type) << 1)) >> 1); -} -} // namespace std diff --git a/src/realm/object-store/sync/sync_user.hpp b/src/realm/object-store/sync/sync_user.hpp index 9be89636c78..c03a9e9204d 100644 --- a/src/realm/object-store/sync/sync_user.hpp +++ b/src/realm/object-store/sync/sync_user.hpp @@ -19,13 +19,10 @@ #ifndef REALM_OS_SYNC_USER_HPP #define REALM_OS_SYNC_USER_HPP -#include +#include #include -#include - #include #include -#include #include #include @@ -35,12 +32,12 @@ namespace realm { namespace app { +class App; struct AppError; -class MongoClient; } // namespace app class SyncManager; class SyncSession; -class SyncUserMetadata; +class UserProvider; // A struct that decodes a given JWT. struct RealmJWT { @@ -54,6 +51,8 @@ struct RealmJWT { // Custom user data embedded in the encoded token. util::Optional user_data; + explicit RealmJWT(std::string_view token); + explicit RealmJWT(StringData token); explicit RealmJWT(const std::string& token); RealmJWT() = default; @@ -63,202 +62,53 @@ struct RealmJWT { } }; -struct SyncUserProfile { - // The full name of the user. - util::Optional name() const - { - return get_field("name"); - } - // The email address of the user. - util::Optional email() const - { - return get_field("email"); - } - // A URL to the user's profile picture. - util::Optional picture_url() const - { - return get_field("picture_url"); - } - // The first name of the user. - util::Optional first_name() const - { - return get_field("first_name"); - } - // The last name of the user. - util::Optional last_name() const - { - return get_field("last_name"); - } - // The gender of the user. - util::Optional gender() const - { - return get_field("gender"); - } - // The birthdate of the user. - util::Optional birthday() const - { - return get_field("birthday"); - } - // The minimum age of the user. - util::Optional min_age() const - { - return get_field("min_age"); - } - // The maximum age of the user. - util::Optional max_age() const - { - return get_field("max_age"); - } - - bson::Bson operator[](const std::string& key) const - { - return m_data.at(key); - } - - const bson::BsonDocument& data() const - { - return m_data; - } - - SyncUserProfile(bson::BsonDocument&& data) - : m_data(std::move(data)) - { - } - SyncUserProfile() = default; - -private: - bson::BsonDocument m_data; - - util::Optional get_field(const char* name) const - { - auto it = m_data.find(name); - if (it == m_data.end()) { - return util::none; - } - return static_cast((*it).second); - } -}; - -// A struct that represents an identity that a `User` is linked to -struct SyncUserIdentity { - // the id of the identity - std::string id; - // the associated provider type of the identity - std::string provider_type; - - SyncUserIdentity(const std::string& id, const std::string& provider_type); - - bool operator==(const SyncUserIdentity& other) const - { - return id == other.id && provider_type == other.provider_type; - } - - bool operator!=(const SyncUserIdentity& other) const - { - return id != other.id || provider_type != other.provider_type; - } +struct SyncUserData { + // Current refresh token or empty if user is logged out + RealmJWT refresh_token; + // Current access token or empty if user is logged out + RealmJWT access_token; + // Current state of the user + UserState state; + // UUIDs which used to be used to generate local Realm file paths. Now only + // used to locate existing files. + std::vector legacy_identities; }; -// A `SyncUser` represents a single user account. Each user manages the sessions that -// are associated with it. -class SyncUser : public std::enable_shared_from_this, public Subscribable { - friend class SyncSession; - struct Private {}; - +class SyncUser { public: - enum class State { - LoggedOut, - LoggedIn, - Removed, - }; - - // Return a list of all sessions belonging to this user. - std::vector> all_sessions() REQUIRES(!m_mutex); - - // Return a session for a given on disk path. - // In most cases, bindings shouldn't expose this to consumers, since the on-disk - // path for a synced Realm is an opaque implementation detail. This API is retained - // for testing purposes, and for bindings for consumers that are servers or tools. - std::shared_ptr session_for_on_disk_path(const std::string& path) REQUIRES(!m_mutex); - - // Log the user out and mark it as such. This will also close its associated Sessions. - void log_out() REQUIRES(!m_mutex, !m_tokens_mutex); - /// Returns true if the users access_token and refresh_token are set. - bool is_logged_in() const REQUIRES(!m_mutex, !m_tokens_mutex); + bool is_logged_in() const REQUIRES(!m_mutex); - /// Returns true if the user's only identity is anonymous. - bool is_anonymous() const REQUIRES(!m_mutex, !m_tokens_mutex); + /// Server-supplied unique id for this user. + const std::string& user_id() const noexcept + { + return m_user_id; + } - const std::string& identity() const noexcept + const std::string& app_id() const noexcept { - return m_identity; + return m_app_id; } const std::vector& legacy_identities() const noexcept { - return m_legacy_identities; + return m_data.legacy_identities; } - std::string access_token() const REQUIRES(!m_tokens_mutex); - std::string refresh_token() const REQUIRES(!m_tokens_mutex); - std::string device_id() const REQUIRES(!m_mutex); - bool has_device_id() const REQUIRES(!m_mutex); - SyncUserProfile user_profile() const REQUIRES(!m_mutex); - std::vector identities() const REQUIRES(!m_mutex); - State state() const REQUIRES(!m_mutex); + std::string access_token() const REQUIRES(!m_mutex); + std::string refresh_token() const REQUIRES(!m_mutex); + UserState state() const REQUIRES(!m_mutex); - // Custom user data embedded in the access token. - util::Optional custom_data() const REQUIRES(!m_tokens_mutex); - - std::shared_ptr sync_manager() const REQUIRES(!m_mutex); - - /// Retrieves a general-purpose service client for the Realm Cloud service - /// @param service_name The name of the cluster - app::MongoClient mongo_client(const std::string& service_name) REQUIRES(!m_mutex); - - // ------------------------------------------------------------------------ - // All of the following are called by `SyncManager` and are public only for - // testing purposes. SDKs should not call these directly in non-test code - // or expose them in the public API. - - explicit SyncUser(Private, const std::string& refresh_token, const std::string& id, - const std::string& access_token, const std::string& device_id, SyncManager* sync_manager); - explicit SyncUser(Private, const SyncUserMetadata& data, SyncManager* sync_manager); - SyncUser(const SyncUser&) = delete; - SyncUser& operator=(const SyncUser&) = delete; + SyncUser(std::shared_ptr, std::shared_ptr, std::string_view app_id, + std::string_view user_id); + virtual ~SyncUser(); // Atomically set the user to be logged in and update both tokens. - void log_in(const std::string& access_token, const std::string& refresh_token) - REQUIRES(!m_mutex, !m_tokens_mutex); - - // Atomically set the user to be removed and remove tokens. - void invalidate() REQUIRES(!m_mutex, !m_tokens_mutex); - - // Update the user's access token. If the user is logged out, it will log itself back in. - // Note that this is called by the SyncManager, and should not be directly called. - void update_access_token(std::string&& token) REQUIRES(!m_mutex, !m_tokens_mutex); - - // Update the user's profile and identities. - void update_user_profile(std::vector identities, SyncUserProfile profile) REQUIRES(!m_mutex); - - // Register a session to this user. - // A registered session will be bound at the earliest opportunity: either - // immediately, or upon the user becoming Active. - // Note that this is called by the SyncManager, and should not be directly called. - void register_session(std::shared_ptr) REQUIRES(!m_mutex); - - /// Refreshes the custom data for this user - /// If `update_location` is true, the location metadata will be queried before the request - void refresh_custom_data(bool update_location, - util::UniqueFunction)> completion_block) - REQUIRES(!m_mutex); - void refresh_custom_data(util::UniqueFunction)> completion_block) - REQUIRES(!m_mutex); + void update_backing_data(SyncUserData&& data) REQUIRES(!m_mutex); /// Checks the expiry on the access token against the local time and if it is invalid or expires soon, returns /// true. - bool access_token_refresh_required() const REQUIRES(!m_tokens_mutex); + bool access_token_refresh_required() const REQUIRES(!m_mutex); // Hook for testing access token timeouts void set_seconds_to_adjust_time_for_testing(int seconds) @@ -266,60 +116,33 @@ class SyncUser : public std::enable_shared_from_this, public Subscriba m_seconds_to_adjust_time_for_testing.store(seconds); } -protected: - friend class SyncManager; - void detach_from_sync_manager() REQUIRES(!m_mutex); + void detach_and_tear_down() REQUIRES(!m_mutex); -private: - bool do_is_anonymous() const REQUIRES(m_mutex); - - std::vector> revive_sessions() REQUIRES(m_mutex); - - State m_state GUARDED_BY(m_mutex); - - // UUIDs which used to be used to generate local Realm file paths. Now only - // used to locate existing files. - std::vector m_legacy_identities; - - mutable util::CheckedMutex m_mutex; - - // Set by the server. The unique ID of the user account on the Realm Application. - const std::string m_identity; - - // Sessions are owned by the SyncManager, but the user keeps a map of weak references - // to them. - std::unordered_map> m_sessions; - - // Waiting sessions are those that should be asked to connect once this user is logged in. - std::unordered_map> m_waiting_sessions; - - mutable util::CheckedMutex m_tokens_mutex; - - // The user's refresh token. - RealmJWT m_refresh_token GUARDED_BY(m_tokens_mutex); - - // The user's access token. - RealmJWT m_access_token GUARDED_BY(m_tokens_mutex); - - // The identities associated with this user. - std::vector m_user_identities GUARDED_BY(m_mutex); - - SyncUserProfile m_user_profile GUARDED_BY(m_mutex); + std::shared_ptr sync_manager() REQUIRES(!m_mutex) + { + util::CheckedLockGuard lock(m_mutex); + return m_sync_manager; + } - const std::string m_device_id; + void request_log_out(util::UniqueFunction)>&&) REQUIRES(!m_mutex); + void request_refresh_user(util::UniqueFunction)>&&) REQUIRES(!m_mutex); + void request_refresh_location(util::UniqueFunction)>&&) REQUIRES(!m_mutex); + void request_access_token(util::UniqueFunction)>&&) REQUIRES(!m_mutex); - SyncManager* m_sync_manager; + void track_realm(std::string_view path) REQUIRES(!m_mutex); + void create_file_action(SyncFileAction action, std::string_view original_path, std::string_view recovery_path, + std::string_view partition_value) REQUIRES(!m_mutex); +protected: + util::CheckedMutex m_mutex; + std::shared_ptr m_provider; + std::shared_ptr m_sync_manager; + const std::string m_app_id; + const std::string m_user_id; + SyncUserData m_data GUARDED_BY(m_mutex); std::atomic m_seconds_to_adjust_time_for_testing = 0; }; } // namespace realm -namespace std { -template <> -struct hash { - size_t operator()(realm::SyncUserIdentity const&) const; -}; -} // namespace std - #endif // REALM_OS_SYNC_USER_HPP diff --git a/src/realm/object-store/sync/user_provider.hpp b/src/realm/object-store/sync/user_provider.hpp new file mode 100644 index 00000000000..7b632a77286 --- /dev/null +++ b/src/realm/object-store/sync/user_provider.hpp @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_USER_PROVIDER_HPP +#define REALM_OS_USER_PROVIDER_HPP + +#include +#include +#include + +namespace realm { +class SyncUser; +namespace app { +struct AppError; +} + +enum class SyncFileAction { + // The Realm files at the given directory will be deleted. + DeleteRealm, + // The Realm file will be copied to a 'recovery' directory, and the original Realm files will be deleted. + BackUpThenDeleteRealm +}; + +enum class UserState { + LoggedOut, + LoggedIn, + Removed, +}; + +class UserProvider { +public: + virtual ~UserProvider() = 0; + + // Called from the constructor of SyncUser. This maybe doesn't need to exist? + virtual void register_sync_user(SyncUser&) = 0; + // Called from SyncUser's destructor. After this returns it is no longer safe + // to reference. + virtual void unregister_sync_user(SyncUser&) = 0; + + // This eventually needs to not be AppError + using CompletionHandler = util::UniqueFunction)>; + // Server has told us to log this user out + virtual void request_log_out(std::string_view user_id, CompletionHandler&&) = 0; + // Server has told us to refresh the user's profile + virtual void request_refresh_user(std::string_view user_id, CompletionHandler&&) = 0; + // Server has told us to refresh the user's location + virtual void request_refresh_location(std::string_view user_id, CompletionHandler&&) = 0; + // Access token is invalid or about to be invalid and needs to be refreshed + virtual void request_access_token(std::string_view user_id, CompletionHandler&&) = 0; + + // Called whenever a Realm is opened with this user in case the provider + // wishes to keep track of them. + virtual void realm_opened(std::string_view user_id, std::string_view path) = 0; + + virtual void create_file_action(std::string_view user_id, SyncFileAction action, std::string_view original_path, + std::string_view recovery_path, std::string_view partition_value) = 0; +}; + +} // namespace realm + +#endif // REALM_OS_USER_PROVIDER_HPP diff --git a/src/realm/sync/socket_provider.hpp b/src/realm/sync/socket_provider.hpp index c9348a7df81..ee51754c7b4 100644 --- a/src/realm/sync/socket_provider.hpp +++ b/src/realm/sync/socket_provider.hpp @@ -18,19 +18,17 @@ #pragma once -#include -#include -#include - #include - #include #include - #include #include #include +#include +#include +#include + namespace realm::sync { namespace websocket { enum class WebSocketError; diff --git a/test/object-store/realm.cpp b/test/object-store/realm.cpp index 7fdbd8918db..db677d8a68d 100644 --- a/test/object-store/realm.cpp +++ b/test/object-store/realm.cpp @@ -47,7 +47,7 @@ #include #include -#include +#include #include #include diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index 4eff1d1dc2d..0459a2a67b7 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include