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