From e7b82a069bd7fa7e3e19d1b4ab423b8b137cdd8a Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 24 Jan 2024 21:27:11 -0800 Subject: [PATCH] Rework sync user handling and metadata storage This introduces the beginning of a split between sync and app services. Object store Sync types (almost) don't depend on the `app` namespace, and can be used independently of it. The SyncUser type now implements only the functionality required internally for sync, and is backed by a UserProvider interface which allows it to request things like access token refreshes. All user management has been removed from SyncManager, and it now only owns the sync client and active sync sessions. SyncSession is mostly unchanged. `app::User` is the new equivalent of the old SyncUser type. The user management which used to be in SyncManager is now in App, which implements the UserProvider interface. Metadata storage for sync and App has been completely redesigned. The metadata store is no longer optional, and instead has an in-memory implementation that should work identically to the persistent store other than not being persistent. The interface has been reworked to enable atomic updates to the metadata store rather than relying on the filesystem mutex in SyncManager, which will be required for multiprocess sync. This required pushing significantly more logic into the metadata storage, which fortunately turned out to also simplify things in the process. The ownership relationship between `App` and `User` has been inverted, with `App` now holding a weak cache of users and `User` strongly retaining the `App`. This ensures that a `SyncConfig` now retains everything it depends on even when app caching is not used. Co-authored-by: James Stone --- CHANGELOG.md | 33 +- bindgen/spec.yml | 109 +- src/realm.h | 40 +- src/realm/error_codes.cpp | 15 +- src/realm/error_codes.h | 2 +- src/realm/error_codes.hpp | 2 +- src/realm/mixed.hpp | 4 + src/realm/obj.hpp | 6 + src/realm/object-store/CMakeLists.txt | 15 +- src/realm/object-store/audit.hpp | 2 + src/realm/object-store/audit.mm | 47 +- src/realm/object-store/c_api/app.cpp | 218 ++-- .../object-store/c_api/notifications.cpp | 2 - src/realm/object-store/c_api/realm.cpp | 2 - src/realm/object-store/c_api/sync.cpp | 28 +- src/realm/object-store/c_api/types.hpp | 28 +- .../object-store/impl/realm_coordinator.cpp | 24 +- src/realm/object-store/property.hpp | 1 - src/realm/object-store/sync/app.cpp | 502 +++++--- src/realm/object-store/sync/app.hpp | 438 +++---- src/realm/object-store/sync/app_config.hpp | 105 ++ .../object-store/sync/app_service_client.hpp | 8 +- src/realm/object-store/sync/app_user.cpp | 311 +++++ src/realm/object-store/sync/app_user.hpp | 267 +++++ src/realm/object-store/sync/app_utils.cpp | 2 +- .../object-store/sync/auth_request_client.hpp | 4 +- .../object-store/sync/impl/app_metadata.cpp | 926 +++++++++++++++ .../object-store/sync/impl/app_metadata.hpp | 77 ++ .../object-store/sync/impl/sync_client.hpp | 8 +- .../object-store/sync/impl/sync_file.cpp | 106 +- .../object-store/sync/impl/sync_file.hpp | 47 +- .../object-store/sync/impl/sync_metadata.cpp | 845 -------------- .../object-store/sync/impl/sync_metadata.hpp | 241 ---- src/realm/object-store/sync/mongo_client.hpp | 16 +- .../object-store/sync/mongo_collection.cpp | 4 +- .../object-store/sync/mongo_collection.hpp | 6 +- .../object-store/sync/mongo_database.cpp | 6 +- .../object-store/sync/mongo_database.hpp | 13 +- src/realm/object-store/sync/push_client.cpp | 4 +- src/realm/object-store/sync/push_client.hpp | 6 +- src/realm/object-store/sync/subscribable.hpp | 1 + src/realm/object-store/sync/sync_manager.cpp | 360 +----- src/realm/object-store/sync/sync_manager.hpp | 210 +--- src/realm/object-store/sync/sync_session.cpp | 67 +- src/realm/object-store/sync/sync_session.hpp | 10 +- src/realm/object-store/sync/sync_user.cpp | 424 ------- src/realm/object-store/sync/sync_user.hpp | 293 +---- src/realm/sync/client.cpp | 8 +- src/realm/sync/client.hpp | 2 +- src/realm/sync/socket_provider.hpp | 10 +- src/realm/util/file.cpp | 11 +- src/realm/util/random.hpp | 2 - test/object-store/CMakeLists.txt | 1 - test/object-store/audit.cpp | 14 +- test/object-store/benchmarks/client_reset.cpp | 2 +- test/object-store/c_api/c_api.cpp | 48 +- test/object-store/realm.cpp | 62 +- test/object-store/sync/app.cpp | 402 ++++--- test/object-store/sync/client_reset.cpp | 2 +- test/object-store/sync/file.cpp | 24 +- test/object-store/sync/flx_migration.cpp | 20 +- test/object-store/sync/flx_sync.cpp | 19 +- test/object-store/sync/metadata.cpp | 1029 ++++++++++------- test/object-store/sync/session/session.cpp | 30 +- .../sync/session/wait_for_completion.cpp | 11 +- test/object-store/sync/sync_manager.cpp | 609 +--------- test/object-store/sync/user.cpp | 303 ----- .../object-store/util/sync/baas_admin_api.hpp | 2 +- .../util/sync/sync_test_utils.cpp | 26 +- .../util/sync/sync_test_utils.hpp | 25 +- test/object-store/util/test_file.cpp | 71 +- test/object-store/util/test_file.hpp | 86 +- .../object-store/util/unit_test_transport.cpp | 2 +- .../object-store/util/unit_test_transport.hpp | 2 - 74 files changed, 3988 insertions(+), 4720 deletions(-) create mode 100644 src/realm/object-store/sync/app_config.hpp create mode 100644 src/realm/object-store/sync/app_user.cpp create mode 100644 src/realm/object-store/sync/app_user.hpp create mode 100644 src/realm/object-store/sync/impl/app_metadata.cpp create mode 100644 src/realm/object-store/sync/impl/app_metadata.hpp delete mode 100644 src/realm/object-store/sync/impl/sync_metadata.cpp delete mode 100644 src/realm/object-store/sync/impl/sync_metadata.hpp delete mode 100644 src/realm/object-store/sync/sync_user.cpp delete mode 100644 test/object-store/sync/user.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a0d6ccee1..97a8968f0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,32 @@ ### Enhancements * (PR [#????](https://github.com/realm/realm-core/pull/????)) -* None. +* Introduce the new `SyncUser` interface which can be implemented by SDKs to use sync without the core App Services implementation (or just for greater control over user behavior in tests). ([PR #7300](https://github.com/realm/realm-core/pull/7300). ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) -* None. - -### Breaking changes -* None. +* SyncUser::all_sessions() included sessions in every state *except* for waiting for access token, which was weirdly inconsistent. It now includes all sessions. ([PR #7300](https://github.com/realm/realm-core/pull/7300)). +* App::all_users() included logged out users only if they were logged out while the App instance existed. It now always includes all logged out users. ([PR #7300](https://github.com/realm/realm-core/pull/7300)). +* Deleting the active user left the active user unset rather than selecting another logged-in user as the active user like logging out and removing users did. ([PR #7300](https://github.com/realm/realm-core/pull/7300)). + +### Breaking changes +* The following things have been renamed or moved as part of moving all of the App Services functionality to the app namespace: + - SyncUser -> app::User. Note that there is a new, different type named SyncUser. + - SyncUser::identity -> app::User::user_id. The "identity" word was overloaded to mean two unrelated things, and one has been changed to user_id everywhere. + - SyncUserSubscriptionToken -> app::UserSubscriptionToken + - SyncUserProfile -> app::UserProfile + - App::Config -> AppConfig + - SyncConfig::MetadataMode -> AppConfig::MetadataMode + - MetadataMode::NoMetadata -> MetadataMode::InMemory + - SyncUser::session_for_on_disk_path() -> SyncManager::get_existing_session() + - SyncUser::all_sessions() -> SyncManager::get_all_sessions_for(User&) + - SyncManager::immediately_run_file_actions() -> App::immediately_run_file_actions() + - realm_sync_user_subscription_token -> realm_app_user_subscription_token + ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* The `ClientAppDeallocated` error code no longer exists as this error code can no longer occur. ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* Some fields have moved from SyncClientConfig to AppConfig. AppConfig now has a SyncClientConfig field rather than it being passed separately to App::get_app(). ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* Sync user management has been removed from SyncManager. This functionality was already additionally available on App. ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* AuditConfig now has a base_file_path field which must be set by the SDK rather than inheriting it from the SyncManager. ([PR #7300](https://github.com/realm/realm-core/pull/7300). ### Compatibility * Fileformat: Generates files with format v24. Reads and automatically upgrade from fileformat v10. If you want to upgrade from an earlier file format version you will have to use RealmCore v13.x.y or earlier. @@ -17,7 +35,9 @@ ----------- ### Internals -* None. +* App metadata storage has been entirely rewritten in preparation for supporting sharing metadata realms between processes. ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* The metadata disabled mode has been replaced with an in-memory metadata mode which performs similarly and doesn't work weirdly differently from the normal mode. The new mode is intended for testing purposes, but should be suitable for production usage if there is a scenario where metadata persistence is not needed. ([PR #7300](https://github.com/realm/realm-core/pull/7300). +* The ownership relationship between App and User has changed. User now strongly retains App and App has a weak cache of Users. This means that creating a SyncConfig or opening a Realm will keep the parent App alive, rather than things being in a broken state if the App is deallocated. ([PR #7300](https://github.com/realm/realm-core/pull/7300). ---------------------------------------------- @@ -182,6 +202,7 @@ ### Breaking changes * SyncManager no longer supports reconfiguring after calling reset_for_testing(). SyncManager::configure() has been folded into the constructor, and reset_for_testing() has been renamed to tear_down_for_testing(). ([PR #7351](https://github.com/realm/realm-core/pull/7351)) + ### Compatibility * Fileformat: Generates files with format v23. Reads and automatically upgrade from fileformat v5. diff --git a/bindgen/spec.yml b/bindgen/spec.yml index 6ba259ac7ac..ac1cc6a3440 100644 --- a/bindgen/spec.yml +++ b/bindgen/spec.yml @@ -226,7 +226,7 @@ enums: - RecoverOrDiscard MetadataMode: - cppName: SyncClientConfig::MetadataMode + cppName: app::AppConfig::MetadataMode values: - NoEncryption - Encryption @@ -287,6 +287,12 @@ enums: - ClientReset - ClientResetNoRecovery + SyncFileAction: + cppName: SyncFileAction + values: + - DeleteRealm + - BackUpThenDeleteRealm + ProgressDirection: cppName: SyncSession::ProgressDirection values: @@ -294,7 +300,7 @@ enums: - download SyncUserState: - cppName: SyncUser::State + cppName: UserState values: - LoggedOut - LoggedIn @@ -450,7 +456,7 @@ records: default: false UserIdentity: - cppName: SyncUserIdentity + cppName: app::UserIdentity fields: id: type: std::string @@ -573,11 +579,6 @@ records: SyncClientConfig: fields: - base_file_path: std::string - metadata_mode: - type: MetadataMode - default: MetadataMode::Encryption - custom_encryption_key: std::optional logger_factory: Nullable log_level: type: LoggerLevel @@ -636,7 +637,7 @@ records: body: std::string DeviceInfo: - cppName: app::App::Config::DeviceInfo + cppName: app::AppConfig::DeviceInfo fields: platform_version: std::string sdk_version: std::string @@ -648,13 +649,19 @@ records: bundle_id: std::string AppConfig: - cppName: app::App::Config + cppName: app::AppConfig fields: app_id: std::string transport: SharedGenericNetworkTransport base_url: std::optional default_request_timeout_ms: std::optional device_info: DeviceInfo + base_file_path: std::string + sync_client_config: SyncClientConfig + metadata_mode: + type: MetadataMode + default: MetadataMode::Encryption + custom_encryption_key: std::optional CompensatingWriteErrorInfo: cppName: sync::CompensatingWriteErrorInfo @@ -1170,36 +1177,49 @@ classes: provider: AuthProvider provider_as_string: std::string - SyncUserSubscriptionToken: - cppName: SyncUser::Token + UserSubscriptionToken: + cppName: app::User::Token SyncUser: sharedPtrWrapped: SharedSyncUser properties: - all_sessions: std::vector is_logged_in: bool - identity: const std::string& - provider_type: const std::string& - local_identity: const std::string& + user_id: std::string + app_id: std::string + legacy_identities: std::vector access_token: std::string refresh_token: std::string + state: SyncUserState + sync_manager: SharedSyncManager + methods: + access_token_refresh_required: bool + request_log_out: '(cb: AsyncCallback<(err: std::optional)>&&)' + request_refresh_user: '(cb: AsyncCallback<(err: std::optional)>&&)' + request_refresh_location: '(cb: AsyncCallback<(err: std::optional)>&&)' + request_access_token: '(cb: AsyncCallback<(err: std::optional)>&&)' + track_realm: '(std::string_view)' + create_file_action: '(action: SyncFileAction, original_path: std::string_view, requested_recovery_dir: std::optional, partition_value: std::string_view) -> std::string' + + User: + base: SyncUser + cppName: app::User + sharedPtrWrapped: SharedUser + properties: + is_anonymous: bool device_id: std::string has_device_id: bool user_profile: UserProfile identities: std::vector custom_data: std::optional - sync_manager: SharedSyncManager - state: SyncUserState subscribers_count: count_t + app: SharedApp methods: log_out: () - session_for_on_disk_path: '(path: StringData) -> Nullable' - subscribe: '(observer: (user: IgnoreArgument)) -> SyncUserSubscriptionToken' - unsubscribe: '(token: SyncUserSubscriptionToken)' - # TODO update methods? + subscribe: '(observer: (user: IgnoreArgument)) -> UserSubscriptionToken' + unsubscribe: '(token: UserSubscriptionToken)' UserProfile: - cppName: SyncUserProfile + cppName: app::UserProfile methods: name: '() -> std::optional' email: '() -> std::optional' @@ -1220,27 +1240,27 @@ classes: sharedPtrWrapped: SharedApp properties: config: const AppConfig& - current_user: Nullable - all_users: std::vector + current_user: Nullable + all_users: std::vector sync_manager: SharedSyncManager subscribers_count: count_t staticMethods: - get_app: '(mode: AppCacheMode, config: AppConfig, sync_client_config: SyncClientConfig) -> SharedApp' + get_app: '(mode: AppCacheMode, config: const AppConfig&) -> SharedApp' get_cached_app: '(app_id: const std::string&) -> SharedApp' clear_cached_apps: () close_all_sync_sessions: () methods: - log_in_with_credentials: '(credentials: AppCredentials, cb: AsyncCallback<(user: const Nullable&, err: std::optional)>&&)' + log_in_with_credentials: '(credentials: AppCredentials, cb: AsyncCallback<(user: const Nullable&, err: std::optional)>&&)' log_out: - '(cb: AsyncCallback<(err: std::optional)>&&)' - - sig: '(user: SharedSyncUser, cb: AsyncCallback<(err: std::optional)>&&)' + - sig: '(user: SharedUser, cb: AsyncCallback<(err: std::optional)>&&)' suffix: user - refresh_custom_data: '(user: SharedSyncUser, cb: AsyncCallback<(err: std::optional)>&&)' - link_user: '(user: SharedSyncUser, credentials: const AppCredentials&, cb: AsyncCallback<(user: const Nullable&, err: std::optional)>&&)' - switch_user: '(user: SharedSyncUser)' - remove_user: '(user: SharedSyncUser, cb: AsyncCallback<(err: std::optional)>&&)' - delete_user: '(user: SharedSyncUser, cb: AsyncCallback<(err: std::optional)>&&)' + refresh_custom_data: '(user: SharedUser, cb: AsyncCallback<(err: std::optional)>&&)' + link_user: '(user: SharedUser, credentials: const AppCredentials&, cb: AsyncCallback<(user: const Nullable&, err: std::optional)>&&)' + switch_user: '(user: SharedUser)' + remove_user: '(user: SharedUser, cb: AsyncCallback<(err: std::optional)>&&)' + delete_user: '(user: SharedUser, cb: AsyncCallback<(err: std::optional)>&&)' usernamePasswordProviderClient: - sig: () -> UsernamePasswordProviderClient cppName: provider_client @@ -1250,8 +1270,8 @@ classes: push_notification_client: '(service_name: const std::string&) -> PushClient' subscribe: '(observer: (app: IgnoreArgument)) -> AppSubscriptionToken' unsubscribe: '(token: AppSubscriptionToken)' - call_function: '(user: const SharedSyncUser&, name: std::string, args: EJsonArray, service_name: std::optional, cb: AsyncCallback<(result: Nullable, err: std::optional)>)' - make_streaming_request: '(user: SharedSyncUser, name: std::string, args: bson::BsonArray, service_name: std::optional) -> Request' + call_function: '(user: const SharedUser&, name: std::string, args: EJsonArray, service_name: std::optional, cb: AsyncCallback<(result: Nullable, err: std::optional)>)' + make_streaming_request: '(user: SharedUser, name: std::string, args: bson::BsonArray, service_name: std::optional) -> Request' update_base_url: '(base_url: std::optional, cb: AsyncCallback<(err: std::optional)>&&)' get_base_url: '() const -> std::string' @@ -1268,8 +1288,8 @@ classes: PushClient: cppName: app::PushClient methods: - register_device: '(registration_token: const std::string&, sync_user: const SharedSyncUser&, completion: AsyncCallback<(err: std::optional)>&&)' - deregister_device: '(sync_user: const SharedSyncUser&, completion: AsyncCallback<(err: std::optional)>&&)' + register_device: '(registration_token: const std::string&, sync_user: const SharedUser&, completion: AsyncCallback<(err: std::optional)>&&)' + deregister_device: '(sync_user: const SharedUser&, completion: AsyncCallback<(err: std::optional)>&&)' UsernamePasswordProviderClient: cppName: app::App::UsernamePasswordProviderClient @@ -1285,12 +1305,12 @@ classes: UserAPIKeyProviderClient: cppName: app::App::UserAPIKeyProviderClient methods: - create_api_key: '(name: const std::string&, user: SharedSyncUser, completion: AsyncCallback<(apiKey: UserAPIKey&&, err: std::optional)>&&)' - fetch_api_key: '(id: ObjectId&, user: const SharedSyncUser, completion: AsyncCallback<(apiKey: UserAPIKey&&, err: std::optional)>&&)' - fetch_api_keys: '(user: const SharedSyncUser, completion: AsyncCallback<(apiKeys: std::vector&&, err: std::optional)>&&)' - delete_api_key: '(id: ObjectId&, user: const SharedSyncUser, completion: AsyncCallback<(err: std::optional)>&&)' - enable_api_key: '(id: ObjectId&, user: const SharedSyncUser, completion: AsyncCallback<(err: std::optional)>&&)' - disable_api_key: '(id: ObjectId&, user: const SharedSyncUser, completion: AsyncCallback<(err: std::optional)>&&)' + create_api_key: '(name: const std::string&, user: SharedUser, completion: AsyncCallback<(apiKey: UserAPIKey&&, err: std::optional)>&&)' + fetch_api_key: '(id: ObjectId&, user: const SharedUser, completion: AsyncCallback<(apiKey: UserAPIKey&&, err: std::optional)>&&)' + fetch_api_keys: '(user: const SharedUser, completion: AsyncCallback<(apiKeys: std::vector&&, err: std::optional)>&&)' + delete_api_key: '(id: ObjectId&, user: const SharedUser, completion: AsyncCallback<(err: std::optional)>&&)' + enable_api_key: '(id: ObjectId&, user: const SharedUser, completion: AsyncCallback<(err: std::optional)>&&)' + disable_api_key: '(id: ObjectId&, user: const SharedUser, completion: AsyncCallback<(err: std::optional)>&&)' # See Helpers::make_loger_factory to construct one. # Using an opaque class here rather than exposing the factory to avoid having to @@ -1307,7 +1327,6 @@ classes: log_level: LoggerLevel has_existing_sessions: bool methods: - immediately_run_file_actions: '(original_name: std::string) -> bool' set_session_multiplexing: '(allowed: bool)' set_log_level: '(level: LoggerLevel)' set_logger_factory: '(factory: LoggerFactory)' @@ -1315,8 +1334,8 @@ classes: set_timeouts: '(timeouts: SyncClientTimeouts)' reconnect: () wait_for_sessions_to_terminate: () - path_for_realm: '(config: SyncConfig, custom_file_name: std::optional) -> StringData' get_existing_active_session: '(path: const std::string&) -> SharedSyncSession' + get_all_sessions_for: '(user: const SyncUser&) -> std::vector' ThreadSafeReference: {} AsyncOpenTask: diff --git a/src/realm.h b/src/realm.h index 4539e4a3383..616c0a2dccc 100644 --- a/src/realm.h +++ b/src/realm.h @@ -2908,6 +2908,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; @@ -3016,6 +3022,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 @@ -3030,14 +3042,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 @@ -3167,11 +3179,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. @@ -3406,9 +3417,9 @@ RLM_API char* realm_app_sync_client_get_default_file_path_for_realm(const realm_ /** * Return the identiy for the user passed as argument * @param user ptr to the user for which the identiy has to be retrieved - * @return a ptr to the identity string + * @return a ptr to the identity string. This must be manually released with realm_free(). */ -RLM_API const char* realm_user_get_identity(const realm_user_t* user) RLM_API_NOEXCEPT; +RLM_API char* realm_user_get_identity(const realm_user_t* user) RLM_API_NOEXCEPT; /** * Retrieve the state for the user passed as argument @@ -3484,12 +3495,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, @@ -3625,7 +3630,7 @@ typedef void (*realm_sync_on_user_state_changed_t)(realm_userdata_t userdata, re typedef struct realm_async_open_task_progress_notification_token realm_async_open_task_progress_notification_token_t; typedef struct realm_sync_session_connection_state_notification_token realm_sync_session_connection_state_notification_token_t; -typedef struct realm_sync_user_subscription_token realm_sync_user_subscription_token_t; +typedef struct realm_app_user_subscription_token realm_app_user_subscription_token_t; /** * Callback function invoked by the async open task once the realm is open and fully synchronized. @@ -3650,11 +3655,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; @@ -4033,7 +4033,7 @@ RLM_API realm_sync_session_connection_state_notification_token_t* realm_sync_ses /** * @return a notification token object. Dispose it to stop receiving notifications. */ -RLM_API realm_sync_user_subscription_token_t* +RLM_API realm_app_user_subscription_token_t* realm_sync_user_on_state_change_register_callback(realm_user_t*, realm_sync_on_user_state_changed_t, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free); diff --git a/src/realm/error_codes.cpp b/src/realm/error_codes.cpp index d8bb59187af..d137afc28f4 100644 --- a/src/realm/error_codes.cpp +++ b/src/realm/error_codes.cpp @@ -161,13 +161,18 @@ ErrorCategory ErrorCodes::error_categories(Error code) .set(ErrorCategory::app_error) .set(ErrorCategory::http_error); - case ClientAppDeallocated: case ClientRedirectError: case ClientTooManyRedirects: + return ErrorCategory() + .set(ErrorCategory::runtime_error) + .set(ErrorCategory::app_error) + .set(ErrorCategory::client_error); + case ClientUserNotFound: case ClientUserNotLoggedIn: + case ClientUserAlreadyNamed: return ErrorCategory() - .set(ErrorCategory::runtime_error) + .set(ErrorCategory::logic_error) .set(ErrorCategory::app_error) .set(ErrorCategory::client_error); @@ -176,7 +181,7 @@ ErrorCategory ErrorCodes::error_categories(Error code) case MalformedJson: case MissingJsonKey: return ErrorCategory() - .set(ErrorCategory::runtime_error) + .set(ErrorCategory::logic_error) .set(ErrorCategory::app_error) .set(ErrorCategory::json_error); @@ -253,7 +258,7 @@ struct MapElem { }; // Note: this array must be kept in sorted order -static const MapElem string_to_error_code[] = { +static const constexpr MapElem string_to_error_code[] = { {"APIKeyAlreadyExists", ErrorCodes::APIKeyAlreadyExists}, {"APIKeyNotFound", ErrorCodes::APIKeyNotFound}, {"AWSError", ErrorCodes::AWSError}, @@ -277,9 +282,9 @@ static const MapElem string_to_error_code[] = { {"BrokenInvariant", ErrorCodes::BrokenInvariant}, {"BrokenPromise", ErrorCodes::BrokenPromise}, {"CallbackFailed", ErrorCodes::CallbackFailed}, - {"ClientAppDeallocated", ErrorCodes::ClientAppDeallocated}, {"ClientRedirectError", ErrorCodes::ClientRedirectError}, {"ClientTooManyRedirects", ErrorCodes::ClientTooManyRedirects}, + {"ClientUserAlreadyNamed", ErrorCodes::ClientUserAlreadyNamed}, {"ClientUserNotFound", ErrorCodes::ClientUserNotFound}, {"ClientUserNotLoggedIn", ErrorCodes::ClientUserNotLoggedIn}, {"ClosedRealm", ErrorCodes::ClosedRealm}, diff --git a/src/realm/error_codes.h b/src/realm/error_codes.h index 1b135bd5d15..7faf9f69a61 100644 --- a/src/realm/error_codes.h +++ b/src/realm/error_codes.h @@ -140,9 +140,9 @@ typedef enum realm_errno { RLM_ERR_CLIENT_USER_NOT_FOUND = 4100, RLM_ERR_CLIENT_USER_NOT_LOGGED_IN = 4101, - RLM_ERR_CLIENT_APP_DEALLOCATED = 4102, RLM_ERR_CLIENT_REDIRECT_ERROR = 4103, RLM_ERR_CLIENT_TOO_MANY_REDIRECTS = 4104, + RLM_ERR_CLIENT_USER_ALREADY_NAMED = 4105, RLM_ERR_BAD_TOKEN = 4200, RLM_ERR_MALFORMED_JSON = 4201, diff --git a/src/realm/error_codes.hpp b/src/realm/error_codes.hpp index 1a25ef9f92f..1d85db0d1c7 100644 --- a/src/realm/error_codes.hpp +++ b/src/realm/error_codes.hpp @@ -184,7 +184,7 @@ class ErrorCodes { ClientUserNotFound = RLM_ERR_CLIENT_USER_NOT_FOUND, ClientUserNotLoggedIn = RLM_ERR_CLIENT_USER_NOT_LOGGED_IN, - ClientAppDeallocated = RLM_ERR_CLIENT_APP_DEALLOCATED, + ClientUserAlreadyNamed = RLM_ERR_CLIENT_USER_ALREADY_NAMED, ClientRedirectError = RLM_ERR_CLIENT_REDIRECT_ERROR, ClientTooManyRedirects = RLM_ERR_CLIENT_TOO_MANY_REDIRECTS, diff --git a/src/realm/mixed.hpp b/src/realm/mixed.hpp index e4fbfb74d99..49460c68be1 100644 --- a/src/realm/mixed.hpp +++ b/src/realm/mixed.hpp @@ -173,6 +173,10 @@ class Mixed { : Mixed(StringData(s)) { } + Mixed(std::string_view s) noexcept + : Mixed(StringData(s)) + { + } Mixed(ref_type ref, CollectionType collection_type) noexcept : m_type(int(collection_type) + 1) diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index edf9db3aa60..a0445b1aedd 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -486,6 +486,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 90177165ad2..55bd7843851 100644 --- a/src/realm/object-store/CMakeLists.txt +++ b/src/realm/object-store/CMakeLists.txt @@ -92,8 +92,10 @@ set(HEADERS if(REALM_ENABLE_SYNC) list(APPEND HEADERS sync/app.hpp + sync/app_config.hpp sync/app_credentials.hpp sync/app_service_client.hpp + sync/app_user.hpp sync/app_utils.hpp sync/async_open_task.hpp sync/auth_request_client.hpp @@ -108,27 +110,28 @@ if(REALM_ENABLE_SYNC) sync/sync_session.hpp sync/sync_user.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_credentials.cpp + sync/app_user.cpp sync/app_utils.cpp sync/async_open_task.cpp sync/generic_network_transport.cpp + sync/impl/app_metadata.cpp sync/impl/sync_file.cpp - sync/impl/sync_metadata.cpp sync/jwt.cpp sync/mongo_client.cpp sync/mongo_collection.cpp sync/mongo_database.cpp sync/push_client.cpp sync/sync_manager.cpp - sync/sync_session.cpp - sync/sync_user.cpp) + sync/sync_session.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..96686f58592 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 base_file_path; }; class AuditInterface { diff --git a/src/realm/object-store/audit.mm b/src/realm/object-store/audit.mm index aa4ba69ca8e..c2abf51b201 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_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) { @@ -1015,8 +1007,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 d9d3430a393..6352910d01f 100644 --- a/src/realm/object-store/c_api/app.cpp +++ b/src/realm/object-store/c_api/app.cpp @@ -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)); }); } @@ -344,22 +358,40 @@ RLM_API bool realm_app_log_out_current_user(realm_app_t* app, realm_app_void_com }); } +namespace { +template +auto with_app_user(const realm_user_t* user, Fn&& fn) +{ + auto app_user = std::dynamic_pointer_cast(*user); + return wrap_err([&] { + if (!app_user) { + throw Exception(ErrorCodes::InvalidArgument, "App Services function require a user obtained from an App"); + } + if constexpr (std::is_void_v) { + fn(app_user); + return true; + } + else { + return fn(app_user); + } + }); +} +} // anonymous namespace + RLM_API bool realm_app_refresh_custom_data(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->refresh_custom_data(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->refresh_custom_data(user, make_callback(callback, userdata, userdata_free)); }); } RLM_API bool realm_app_log_out(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->log_out(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->log_out(user, make_callback(callback, userdata, userdata_free)); }); } @@ -367,38 +399,31 @@ RLM_API bool realm_app_link_user(realm_app_t* app, realm_user_t* user, realm_app realm_app_user_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->link_user(*user, *credentials, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->link_user(user, *credentials, make_callback(callback, userdata, userdata_free)); }); } -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)); - } - return true; + return with_app_user(user, [&](auto& user) { + (*app)->switch_user(user); }); } RLM_API bool realm_app_remove_user(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->remove_user(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->remove_user(user, make_callback(callback, userdata, userdata_free)); }); } RLM_API bool realm_app_delete_user(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->delete_user(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->delete_user(user, make_callback(callback, userdata, userdata_free)); }); } @@ -492,10 +517,9 @@ RLM_API bool realm_app_user_apikey_provider_client_create_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().create_api_key( - name, *user, make_callback(callback, userdata, userdata_free)); - return true; + name, user, make_callback(callback, userdata, userdata_free)); }); } @@ -505,10 +529,9 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikey(const realm_app_ realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().fetch_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -517,7 +540,7 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikeys(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}]( std::vector apikeys, util::Optional error) { if (error) { @@ -534,8 +557,7 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikeys(const realm_app } }; - (*app)->provider_client().fetch_api_keys(*user, std::move(cb)); - return true; + (*app)->provider_client().fetch_api_keys(user, std::move(cb)); }); } @@ -545,10 +567,9 @@ RLM_API bool realm_app_user_apikey_provider_client_delete_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().delete_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -558,10 +579,9 @@ RLM_API bool realm_app_user_apikey_provider_client_enable_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().enable_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -571,10 +591,9 @@ RLM_API bool realm_app_user_apikey_provider_client_disable_apikey(const realm_ap realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().disable_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -582,11 +601,10 @@ RLM_API bool realm_app_push_notification_client_register_device( const realm_app_t* app, const realm_user_t* user, const char* service_name, const char* registration_token, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app) ->push_notification_client(service_name) - .register_device(registration_token, *user, make_callback(callback, userdata, userdata_free)); - return true; + .register_device(registration_token, user, make_callback(callback, userdata, userdata_free)); }); } @@ -596,11 +614,10 @@ RLM_API bool realm_app_push_notification_client_deregister_device(const realm_ap realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app) ->push_notification_client(service_name) - .deregister_device(*user, make_callback(callback, userdata, userdata_free)); - return true; + .deregister_device(user, make_callback(callback, userdata, userdata_free)); }); } @@ -609,7 +626,7 @@ RLM_API bool realm_app_call_function(const realm_app_t* app, const realm_user_t* realm_return_string_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}]( const std::string* reply, util::Optional error) { if (error) { @@ -622,8 +639,7 @@ RLM_API bool realm_app_call_function(const realm_app_t* app, const realm_user_t* }; util::Optional service_name_opt = service_name ? util::some(service_name) : util::none; - (*app)->call_function(*user, function_name, serialized_ejson_payload, service_name_opt, std::move(cb)); - return true; + (*app)->call_function(user, function_name, serialized_ejson_payload, service_name_opt, std::move(cb)); }); } @@ -645,17 +661,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 +RLM_API char* realm_user_get_identity(const realm_user_t* user) noexcept { - return (*user)->identity().c_str(); + return duplicate_string((*user)->user_id()); } RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) noexcept @@ -666,8 +686,8 @@ RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) noexce RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_identity_t* out_identities, size_t max, size_t* out_n) { - return wrap_err([&] { - const auto& identities = (*user)->identities(); + return with_app_user(user, [&](auto& user) { + const auto& identities = user->identities(); set_out_param(out_n, identities.size()); if (out_identities && max >= identities.size()) { for (size_t i = 0; i < identities.size(); i++) { @@ -675,24 +695,24 @@ RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_ realm_auth_provider_e(enum_from_provider_type(identities[i].provider_type))}; } } - return true; }); } RLM_API char* realm_user_get_device_id(const realm_user_t* user) noexcept { - if ((*user)->has_device_id()) { - return duplicate_string((*user)->device_id()); - } - - return nullptr; + char* device_id = nullptr; + with_app_user(user, [&](auto& user) { + if (user->has_device_id()) { + device_id = duplicate_string(user->device_id()); + } + }); + return device_id; } RLM_API bool realm_user_log_out(realm_user_t* user) { - return wrap_err([&] { - (*user)->log_out(); - return true; + return with_app_user(user, [&](auto& user) { + user->log_out(); }); } @@ -703,20 +723,21 @@ RLM_API bool realm_user_is_logged_in(const realm_user_t* user) noexcept RLM_API char* realm_user_get_profile_data(const realm_user_t* user) { - return wrap_err([&] { - std::string data = bson::Bson((*user)->user_profile().data()).to_string(); + return with_app_user(user, [&](auto& user) { + std::string data = bson::Bson(user->user_profile().data()).to_string(); return duplicate_string(data); }); } RLM_API char* realm_user_get_custom_data(const realm_user_t* user) noexcept { - if (const auto& data = (*user)->custom_data()) { - std::string json = bson::Bson(*data).to_string(); - return duplicate_string(json); - } - - return nullptr; + return with_app_user(user, [&](auto& user) -> char* { + if (const auto& data = user->custom_data()) { + std::string json = bson::Bson(*data).to_string(); + return duplicate_string(json); + } + return nullptr; + }); } RLM_API char* realm_user_get_access_token(const realm_user_t* user) @@ -736,39 +757,35 @@ 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 with_app_user(user, [&](auto& user) { + return new realm_app_t(user->app()); + }); } -RLM_API realm_sync_user_subscription_token_t* +RLM_API realm_app_user_subscription_token_t* realm_sync_user_on_state_change_register_callback(realm_user_t* user, realm_sync_on_user_state_changed_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}](const SyncUser& sync_user) { callback(userdata.get(), realm_user_state_e(sync_user.state())); }; - auto token = (*user)->subscribe(std::move(cb)); - return new realm_sync_user_subscription_token_t{*user, std::move(token)}; + auto token = user->subscribe(std::move(cb)); + return new realm_app_user_subscription_token_t{user, std::move(token)}; }); } +namespace { template -inline util::Optional convert_to_optional(T data) +util::Optional convert_to_optional(T data) { return data ? util::Optional(data) : util::Optional(); } template -inline util::Optional convert_to_optional_bson(realm_string_t doc) +util::Optional convert_to_optional_bson(realm_string_t doc) { if (doc.data == nullptr || doc.size == 0) { return util::Optional(); @@ -777,13 +794,13 @@ inline util::Optional convert_to_optional_bson(realm_string_t doc) } template -inline T convert_to_bson(realm_string_t doc) +T convert_to_bson(realm_string_t doc) { auto res = convert_to_optional_bson(doc); return res ? *res : T(); } -static MongoCollection::FindOptions to_mongodb_collection_find_options(const realm_mongodb_find_options_t* options) +MongoCollection::FindOptions to_mongodb_collection_find_options(const realm_mongodb_find_options_t* options) { MongoCollection::FindOptions mongodb_options; mongodb_options.projection_bson = convert_to_optional_bson(options->projection_bson); @@ -792,7 +809,7 @@ static MongoCollection::FindOptions to_mongodb_collection_find_options(const rea return mongodb_options; } -static MongoCollection::FindOneAndModifyOptions +MongoCollection::FindOneAndModifyOptions to_mongodb_collection_find_one_and_modify_options(const realm_mongodb_find_one_and_modify_options_t* options) { MongoCollection::FindOneAndModifyOptions mongodb_options; @@ -803,8 +820,8 @@ to_mongodb_collection_find_one_and_modify_options(const realm_mongodb_find_one_a return mongodb_options; } -static void handle_mongodb_collection_result(util::Optional bson, util::Optional app_error, - UserdataPtr data, realm_mongodb_callback_t callback) +void handle_mongodb_collection_result(util::Optional bson, util::Optional app_error, + UserdataPtr data, realm_mongodb_callback_t callback) { if (app_error) { auto error = to_capi(*app_error); @@ -815,6 +832,7 @@ static void handle_mongodb_collection_result(util::Optional bson, ut callback(data.get(), {bson_data.c_str(), bson_data.size()}, nullptr); } } +} // anonymous namespace RLM_API realm_mongodb_collection_t* realm_mongo_collection_get(realm_user_t* user, const char* service, const char* database, const char* collection) @@ -823,8 +841,8 @@ RLM_API realm_mongodb_collection_t* realm_mongo_collection_get(realm_user_t* use REALM_ASSERT(service); REALM_ASSERT(database); REALM_ASSERT(collection); - return wrap_err([&]() { - auto col = (*user)->mongo_client(service).db(database).collection(collection); + return with_app_user(user, [&](auto& user) { + auto col = user->mongo_client(service).db(database).collection(collection); return new realm_mongodb_collection_t(col); }); } diff --git a/src/realm/object-store/c_api/notifications.cpp b/src/realm/object-store/c_api/notifications.cpp index 666e3110fcf..5bb041f35f6 100644 --- a/src/realm/object-store/c_api/notifications.cpp +++ b/src/realm/object-store/c_api/notifications.cpp @@ -210,8 +210,6 @@ RLM_API void realm_collection_changes_get_num_changes(const realm_collection_cha bool* out_collection_was_cleared, bool* out_collection_was_deleted) { - // FIXME: This has O(n) performance, which seems ridiculous. - if (out_num_deletions) *out_num_deletions = changes->deletions.count(); if (out_num_insertions) diff --git a/src/realm/object-store/c_api/realm.cpp b/src/realm/object-store/c_api/realm.cpp index edf1d5ad819..d0db803ca57 100644 --- a/src/realm/object-store/c_api/realm.cpp +++ b/src/realm/object-store/c_api/realm.cpp @@ -319,8 +319,6 @@ RLM_API realm_t* realm_from_thread_safe_reference(realm_thread_safe_reference_t* throw LogicError{ErrorCodes::IllegalOperation, "Thread safe reference type mismatch"}; } - // FIXME: This moves out of the ThreadSafeReference, so it isn't - // reusable. std::shared_ptr sch; if (scheduler) { sch = *scheduler; diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 27ae222b61f..314cf831efd 100644 --- a/src/realm/object-store/c_api/sync.cpp +++ b/src/realm/object-store/c_api/sync.cpp @@ -40,18 +40,18 @@ realm_sync_session_connection_state_notification_token::~realm_sync_session_conn session->unregister_connection_change_callback(token); } -realm_sync_user_subscription_token::~realm_sync_user_subscription_token() +realm_app_user_subscription_token::~realm_app_user_subscription_token() { user->unsubscribe(token); } 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); @@ -141,24 +141,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 { @@ -775,7 +757,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 317fc09fec9..6e7c6894521 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 @@ -570,15 +570,15 @@ struct realm_results : realm::c_api::WrapC, realm::Results { #if REALM_ENABLE_SYNC -struct realm_sync_user_subscription_token : realm::c_api::WrapC { - using Token = realm::Subscribable::Token; - realm_sync_user_subscription_token(std::shared_ptr user, Token&& token) +struct realm_app_user_subscription_token : realm::c_api::WrapC { + using Token = realm::Subscribable::Token; + realm_app_user_subscription_token(std::shared_ptr user, Token&& token) : user(user) , token(std::move(token)) { } - ~realm_sync_user_subscription_token(); - std::shared_ptr user; + ~realm_app_user_subscription_token(); + std::shared_ptr user; Token token; }; @@ -625,8 +625,8 @@ struct realm_http_transport : realm::c_api::WrapC, std::shared_ptr #if REALM_ENABLE_SYNC -#include #include #include #include @@ -133,6 +132,10 @@ void RealmCoordinator::set_config(const Realm::Config& config) throw InvalidArgument(ErrorCodes::IllegalCombination, "Cannot specify a partition value when flexible sync is enabled"); } + if (!config.sync_config->user) { + throw InvalidArgument(ErrorCodes::IllegalCombination, + "A user must be provided to open a synchronized Realm."); + } } #endif @@ -188,7 +191,6 @@ void RealmCoordinator::set_config(const Realm::Config& config) ErrorCodes::MismatchedConfig, util::format("Realm at path '%1' already opened with different sync user.", config.path)); } - if (m_config.sync_config->partition_value != config.sync_config->partition_value) { throw LogicError( ErrorCodes::MismatchedConfig, @@ -433,10 +435,13 @@ bool RealmCoordinator::open_db() #if REALM_ENABLE_SYNC if (m_config.sync_config) { + REALM_ASSERT(m_config.sync_config->user); // 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(); @@ -538,8 +543,17 @@ void RealmCoordinator::init_external_helpers() #if REALM_ENABLE_SYNC // We may have reused an existing sync session that outlived its original // RealmCoordinator. If not, we need to create a new one now. - if (m_config.sync_config && !m_sync_session) - m_sync_session = m_config.sync_config->user->sync_manager()->get_session(m_db, m_config); + if (m_config.sync_config && !m_sync_session) { + if (!m_config.sync_config->user || m_config.sync_config->user->state() == SyncUser::State::Removed) { + throw app::AppError( + ErrorCodes::ClientUserNotFound, + util::format("Cannot start a sync session for user '%1' because this user has been removed.", + m_config.sync_config->user->user_id())); + } + if (auto sync_manager = m_config.sync_config->user->sync_manager()) { + m_sync_session = sync_manager->get_session(m_db, m_config); + } + } #endif if (!m_notifier && !m_config.immutable() && m_config.automatic_change_notifications) { diff --git a/src/realm/object-store/property.hpp b/src/realm/object-store/property.hpp index 02af9e6d3e3..a2501785f59 100644 --- a/src/realm/object-store/property.hpp +++ b/src/realm/object-store/property.hpp @@ -21,7 +21,6 @@ #include #include -// FIXME: keys.hpp is currently pretty heavyweight #include #include #include diff --git a/src/realm/object-store/sync/app.cpp b/src/realm/object-store/sync/app.cpp index 0ad8220095f..85a985a0144 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 @@ -139,7 +142,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"}}; @@ -178,59 +181,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; @@ -245,13 +219,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(); @@ -259,11 +233,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)) + , m_metadata_store(create_metadata_store(config, *m_file_manager)) + , m_sync_manager(SyncManager::create(config.sync_client_config)) { #ifdef __EMSCRIPTEN__ if (!m_config.transport) { @@ -271,7 +247,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) { @@ -297,24 +272,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 = SyncManager::create(shared_from_this(), {}, sync_client_config, config().app_id); -} - 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); } @@ -366,7 +330,6 @@ 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, @@ -493,7 +456,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; @@ -505,7 +468,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; @@ -517,7 +480,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; @@ -546,7 +509,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; @@ -557,7 +520,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; @@ -568,7 +531,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; @@ -580,14 +543,71 @@ void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, c } // MARK: - App -std::shared_ptr App::current_user() const +// The user cache can have an expired pointer to an object if another thread is +// currently waiting for the mutex so that it can unregister the object, which +// will result in shared_from_this() throwing. We could instead do +// `weak_from_this().lock()`, but that is more expensive in the much more common +// case where the pointer is valid. +// +// Storing weak_ptrs in m_user would also avoid this problem, but would introduce +// a different one where the natural way to use the users could result in us +// trying to release the final strong reference while holding the lock, which +// would lead to a deadlock +static std::shared_ptr try_lock(User& user) +{ + try { + return user.shared_from_this(); + } + catch (const std::bad_weak_ptr&) { + return nullptr; + } +} + +std::shared_ptr App::get_user_for_id(const std::string& user_id) +{ + if (auto& user = m_users[user_id]) { + if (auto locked = try_lock(*user)) { + return locked; + } + } + return User::make(shared_from_this(), user_id); +} + +void App::user_data_updated(const std::string& user_id) { - return m_sync_manager->get_current_user(); + 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()) { + if (auto user = try_lock(*m_current_user)) { + return user; + } + } + 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::vector> App::all_users() const +std::shared_ptr App::get_existing_logged_in_user(std::string_view user_id) { - return m_sync_manager->all_users(); + 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()) { + if (auto user = try_lock(*it->second)) { + return user; + } + } + if (m_metadata_store->has_logged_in_user(user_id)) { + return User::make(shared_from_this(), user_id); + } + return nullptr; } std::string App::get_base_url() const @@ -628,8 +648,20 @@ void App::update_base_url(std::string_view new_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_all_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; @@ -638,8 +670,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)); } @@ -648,53 +681,53 @@ 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); + if (auto data = m_metadata_store->get_user(user->user_id())) { + data->identities = std::move(identities); + data->profile = UserProfile(get(profile_json, "data")); + m_metadata_store->update_user(user->user_id(), *data); + user->update_backing_data(std::move(data)); + } } catch (const AppError& err) { return completion(nullptr, err); } - return completion(sync_user, {}); + return completion(user, {}); }); } 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); + util::get_library_platform(), m_config.device_info.platform_version, m_config.device_info.sdk, + m_config.device_info.sdk_version, REALM_VERSION_STRING); + + BsonDocument options; 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); @@ -702,15 +735,34 @@ 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 = try_lock(*user); + if (!anon_user) + continue; + 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" : ""); @@ -721,60 +773,75 @@ 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")); + if (auto user_data = m_metadata_store->get_user(linking_user->user_id())) { + user_data->access_token = RealmJWT(get(json, "access_token")); + // maybe a callback for this? + m_metadata_store->update_user(linking_user->user_id(), *user_data); + 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)); + // If the user has not been logged in, then there is a problem with the token + if (!sync_user->is_logged_in()) { + return completion(nullptr, + AppError(ErrorCodes::BadToken, "Could not log in user: received malformed JWT")); + } + 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, SyncUser::State new_state, + UniqueFunction)>&& completion) { - if (!user || user->state() != SyncUser::State::LoggedIn) { - log_debug("App: log_out() - already logged out"); - return completion(util::none); + if (!user || user->state() == new_state || user->state() == SyncUser::State::Removed) { + if (completion) { + completion(util::none); + } + return; } - 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); + + m_metadata_store->log_out(user->user_id(), new_state); + user->update_backing_data(m_metadata_store->get_user(user->user_id())); do_request(std::move(req), [self = shared_from_this(), completion = std::move(completion)](const Response& response) { @@ -782,61 +849,80 @@ void App::log_out(const std::shared_ptr& user, UniqueFunctionemit_change_to_subscribers(*self); } - completion(error); + if (completion) { + completion(error); + } }); } +void App::log_out(const std::shared_ptr& user, UniqueFunction)>&& completion) +{ + auto new_state = user && user->is_anonymous() ? SyncUser::State::Removed : SyncUser::State::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) { 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::remove_user(const std::shared_ptr& user, UniqueFunction)>&& completion) { if (!user || user->state() == SyncUser::State::Removed) { return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed")); } - if (!verify_user_present(user)) { - return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found")); + + { + util::CheckedLockGuard lock(m_user_mutex); + if (!verify_user_present(user)) { + return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found")); + } } if (user->is_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()); - return completion(error); - }); + log_out( + user, SyncUser::State::Removed, + [user, completion = std::move(completion), self = shared_from_this()](const Optional& error) { + user->update_backing_data(std::nullopt); + if (completion) { + completion(error); + } + }); } else { - m_sync_manager->remove_user(user->identity()); - return completion({}); + m_metadata_store->log_out(user->user_id(), SyncUser::State::Removed); + user->update_backing_data(std::nullopt); + if (completion) { + completion(std::nullopt); + } } } -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.")); @@ -845,28 +931,33 @@ 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, @@ -876,20 +967,21 @@ void App::link_user(const std::shared_ptr& user, const AppCredentials& 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) { + return completion(nullptr, AppError(ErrorCodes::ClientUserAlreadyNamed, + "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)); @@ -906,9 +998,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, @@ -1000,13 +1090,10 @@ 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); @@ -1125,7 +1212,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 @@ -1146,7 +1233,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 @@ -1164,25 +1251,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) { @@ -1202,14 +1287,19 @@ void App::refresh_access_token(const std::shared_ptr& sync_user, bool do_request( {HttpMethod::post, url_for_path("/auth/session"), m_request_timeout_ms, get_request_headers(sync_user, RequestTokenType::RefreshToken)}, - [completion = std::move(completion), sync_user](const Response& response) { + [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& response) { if (auto error = AppUtils::check_for_errors(response)) { return completion(std::move(error)); } try { auto json = parse(response.body); - sync_user->update_access_token(get(json, "access_token")); + RealmJWT access_token{get(json, "access_token")}; + if (auto data = self->m_metadata_store->get_user(sync_user->user_id())) { + data->access_token = access_token; + self->m_metadata_store->update_user(sync_user->user_id(), *data); + sync_user->update_backing_data(std::move(data)); + } } catch (AppError& err) { return completion(std::move(err)); @@ -1226,7 +1316,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) { @@ -1251,7 +1341,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) { @@ -1293,7 +1383,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)); @@ -1303,16 +1393,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{ @@ -1343,7 +1433,43 @@ 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(User& user) +{ + auto& tracked_user = m_users[user.user_id()]; + REALM_ASSERT(!tracked_user || !tracked_user->weak_from_this().lock()); + tracked_user = &user; + user.update_backing_data(m_metadata_store->get_user(user.user_id())); +} + +void App::unregister_sync_user(User& user) +{ + util::CheckedLockGuard lock(m_user_mutex); + auto it = m_users.find(user.user_id()); + REALM_ASSERT(it != m_users.end()); + // If the user was requested while we were waiting for the lock, it may + // have already been replaced with a new instance for the same user id + if (it != m_users.end() && it->second == &user) { + m_users.erase(it); + } + if (m_current_user == &user) { + m_current_user = nullptr; + } +} + +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 c5cf5c4a15a..bfe2661bf27 100644 --- a/src/realm/object-store/sync/app.hpp +++ b/src/realm/object-store/sync/app.hpp @@ -19,30 +19,29 @@ #ifndef REALM_APP_HPP #define REALM_APP_HPP +#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 SyncFileManager; namespace app { class App; +class MetadataStore; +class User; typedef std::shared_ptr SharedApp; @@ -52,67 +51,133 @@ 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, 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 auth_request_client() + { + return std::shared_ptr(shared_from_this(), this); + } - std::shared_ptr const& sync_manager() const + std::shared_ptr app_service_client() { - return m_sync_manager; + 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 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&, std::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&, std::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. @@ -120,7 +185,7 @@ class App : public std::enable_shared_from_this, /// The actual key. Will only be included in /// the response when an API key is first created. - util::Optional key; + std::optional key; /// The name of the key. std::string name; @@ -137,41 +202,41 @@ 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, - util::UniqueFunction)>&& completion); + 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, - util::UniqueFunction)>&& completion); + 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, - util::UniqueFunction&&, util::Optional)>&& completion); + fetch_api_keys(const std::shared_ptr& user, + util::UniqueFunction&&, std::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, - util::UniqueFunction)>&& completion); + 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, - util::UniqueFunction)>&& completion); + 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, - util::UniqueFunction)>&& completion); + void disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); private: friend class App; @@ -196,30 +261,30 @@ class App : public std::enable_shared_from_this, /// @param password The password that the user created for the new username/password identity. /// @param completion A callback to be invoked once the call is complete. void register_email(const std::string& email, const std::string& password, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Confirms an email identity with the username/password provider. /// @param token The confirmation token that was emailed to the user. /// @param token_id The confirmation token id that was emailed to the user. /// @param completion A callback to be invoked once the call is complete. void confirm_user(const std::string& token, const std::string& token_id, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Re-sends a confirmation email to a user that has registered but /// not yet confirmed their email address. /// @param email The email address of the user to re-send a confirmation for. /// @param completion A callback to be invoked once the call is complete. void resend_confirmation_email(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); void send_reset_password_email(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Retries the custom confirmation function on a user for a given email. /// @param email The email address of the user to retry the custom confirmation for. /// @param completion A callback to be invoked once the retry is complete. void retry_custom_confirmation(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Resets the password of an email identity using the /// password reset token emailed to a user. @@ -228,7 +293,7 @@ class App : public std::enable_shared_from_this, /// @param token_id The password reset token id that was emailed to the user. /// @param completion A callback to be invoked once the call is complete. void reset_password(const std::string& password, const std::string& token, const std::string& token_id, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Resets the password of an email identity using the /// password reset function set up in the application. @@ -238,7 +303,7 @@ class App : public std::enable_shared_from_this, /// @param completion A callback to be invoked once the call is complete. void call_reset_password_function(const std::string& email, const std::string& password, const bson::BsonArray& args, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); private: friend class App; @@ -250,21 +315,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. @@ -285,114 +347,41 @@ class App : public std::enable_shared_from_this, void update_base_url(std::string_view 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) + void call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, + const std::optional& service_name, + util::UniqueFunction)>&& completion) final 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) + call_function(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::optional& service_name, + util::UniqueFunction&&, std::optional)>&& completion) final 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, - 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 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, - util::UniqueFunction&&, util::Optional)>&& completion) final + void + call_function(const std::shared_ptr& user, const std::string&, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::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); + void + call_function(const std::string& name, const bson::BsonArray& args_bson, + const std::optional& service_name, + util::UniqueFunction&&, std::optional)>&& completion) final + 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); + void + call_function(const std::string&, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::optional)>&& completion) final + 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, - util::UniqueFunction&&, util::Optional)>&& completion) + void call_function(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::optional)>&& completion) REQUIRES(!m_route_mutex) { call_function( user, name, args_bson, util::none, - [completion = std::move(completion)](util::Optional&& value, util::Optional error) { + [completion = std::move(completion)](std::optional&& value, std::optional error) { if (value) { return completion(util::some(static_cast(*value)), std::move(error)); } @@ -403,8 +392,8 @@ class App : public std::enable_shared_from_this, template void call_function(const std::string& name, const bson::BsonArray& args_bson, - util::UniqueFunction&&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex) + util::UniqueFunction&&, std::optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex) { call_function(current_user(), name, args_bson, std::move(completion)); @@ -412,31 +401,37 @@ 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); + const std::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); + // 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; + + // 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 Config m_config; + 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 @@ -454,14 +449,16 @@ class App : public std::enable_shared_from_this, // (e.g. "https://us-east-1.aws.realm.mongodb.com/api/client/v2.0/app//auth") std::string m_auth_route GUARDED_BY(m_route_mutex); // If false, the location info will be updated upon the next AppServices request - bool m_location_updated GUARDED_BY(m_route_mutex); + bool m_location_updated GUARDED_BY(m_route_mutex) = false; // Storage for the location info returned by the base URL location endpoint // Base hostname for AppServices HTTP requests std::string m_host_url GUARDED_BY(m_route_mutex); // 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; @@ -477,11 +474,11 @@ 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, - util::UniqueFunction)>&& completion) + void refresh_access_token(const std::shared_ptr& user, bool update_location, + util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); /// Checks if an auth failure has taken place and if so it will attempt to refresh the @@ -492,7 +489,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); @@ -500,7 +497,7 @@ class App : public std::enable_shared_from_this, /// Return the app route for this App instance, or creates a new app route string if /// a new hostname is provided /// @param hostname The hostname to generate a new app route - std::string get_app_route(const util::Optional& hostname = util::none) const REQUIRES(m_route_mutex); + std::string get_app_route(const std::optional& hostname = util::none) const REQUIRES(m_route_mutex); /// Request the app metadata information from the server if it has not been processed yet. If /// a new hostname is provided, the app metadata will be refreshed using the new hostname. @@ -508,7 +505,7 @@ class App : public std::enable_shared_from_this, /// @param new_hostname The (Original) new hostname to request the location from /// @param redir_location The location provided by the last redirect response when querying location /// @param redirect_count The current number of redirects that have occurred in a row - void request_location(util::UniqueFunction)>&& completion, + void request_location(util::UniqueFunction)>&& completion, std::optional&& new_hostname = std::nullopt, std::optional&& redir_location = std::nullopt, int redirect_count = 0) REQUIRES(!m_route_mutex); @@ -525,9 +522,9 @@ class App : public std::enable_shared_from_this, /// @param completion The original completion object that will be called with the response to the request /// @param new_hostname If provided, the metadata will be requested from this hostname void update_location_and_resend(Request&& request, util::UniqueFunction&& completion, - util::Optional&& new_hostname = util::none) REQUIRES(!m_route_mutex); + std::optional&& new_hostname = util::none) REQUIRES(!m_route_mutex); - void post(std::string&& route, util::UniqueFunction)>&& completion, + void post(std::string&& route, util::UniqueFunction)>&& completion, const bson::BsonDocument& body) REQUIRES(!m_route_mutex); /// Performs a request to the Stitch server. This request does not contain authentication state. @@ -548,38 +545,41 @@ 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&, std::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&, std::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); std::string make_sync_route(util::Optional ws_host_url = util::none) REQUIRES(m_route_mutex); void configure_route(const std::string& host_url, const std::string& ws_host_url) REQUIRES(m_route_mutex); @@ -588,7 +588,23 @@ class App : public std::enable_shared_from_this, 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(User& sync_user) REQUIRES(m_user_mutex); + void unregister_sync_user(User& user) 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 log_out(const std::shared_ptr& user, SyncUser::State 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..c78a1b0979a --- /dev/null +++ b/src/realm/object-store/sync/app_user.cpp @@ -0,0 +1,311 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 + +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) + : m_app(std::move(app)) + , m_app_id(m_app->config().app_id) + , m_user_id(user_id) +{ + m_app->register_sync_user(*this); +} + +User::~User() +{ + if (m_app) { + m_app->unregister_sync_user(*this); + } +} + +std::string User::user_id() const noexcept +{ + return m_user_id; +} + +std::string User::app_id() const noexcept +{ + return m_app_id; +} + +std::vector User::legacy_identities() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.legacy_identities; +} + +std::string User::access_token() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.access_token.token; +} + +std::string User::refresh_token() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.refresh_token.token; +} + +SyncUser::State User::state() const +{ + util::CheckedLockGuard lock(m_mutex); + if (!m_app) + return SyncUser::State::Removed; + return m_data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut; +} + +bool User::is_anonymous() const +{ + util::CheckedLockGuard lock(m_mutex); + return do_is_anonymous(); +} + +bool User::do_is_anonymous() const +{ + return m_data.access_token && m_data.identities.size() == 1 && + m_data.identities[0].provider_type == app::IdentityProviderAnonymous; +} + +std::string User::device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.device_id; +} + +bool User::has_device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return !m_data.device_id.empty() && m_data.device_id != "000000000000000000000000"; +} + +UserProfile User::user_profile() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.profile; +} + +std::vector User::identities() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.identities; +} + +std::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); + return m_app; +} + +SyncManager* User::sync_manager() +{ + util::CheckedLockGuard lock(m_mutex); + return m_app ? m_app->sync_manager().get() : nullptr; +} + +app::MongoClient User::mongo_client(const std::string& service_name) +{ + util::CheckedLockGuard lock(m_mutex); + return app::MongoClient(shared_from_this(), m_app->app_service_client(), service_name); +} + +bool User::access_token_refresh_required() const +{ + using namespace std::chrono; + constexpr size_t buffer_seconds = 5; // arbitrary + util::CheckedLockGuard lock(m_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_data.access_token && m_data.access_token.expires_at < static_cast(threshold); +} + +void User::log_out() +{ + if (auto app = this->app()) { + app->log_out(shared_from_this(), nullptr); + } +} + +void User::detach_and_tear_down() +{ + std::shared_ptr app; + { + util::CheckedLockGuard lk(m_mutex); + m_data.access_token.token.clear(); + m_data.refresh_token.token.clear(); + app = std::exchange(m_app, nullptr); + } + + if (app) { + app->sync_manager()->update_sessions_for(*this, SyncUser::State::LoggedIn, SyncUser::State::Removed, {}); + app->unregister_sync_user(*this); + } +} + +void User::update_data_for_testing(util::FunctionRef fn) +{ + UserData data; + { + util::CheckedLockGuard lock(m_mutex); + data = m_data; + } + fn(data); + update_backing_data(std::move(data)); +} + +void User::update_backing_data(std::optional&& data) +{ + if (!data) { + detach_and_tear_down(); + emit_change_to_subscribers(*this); + return; + } + + std::string new_token; + SyncUser::State old_state; + SyncUser::State new_state = data->access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut; + std::shared_ptr sync_manager; + { + util::CheckedLockGuard lock(m_mutex); + if (!m_app) { + return; // is already detached + } + sync_manager = m_app->sync_manager(); + old_state = m_data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut; + if (new_state == SyncUser::State::LoggedIn && data->access_token != m_data.access_token) + new_token = data->access_token.token; + m_data = std::move(*data); + } + + sync_manager->update_sessions_for(*this, old_state, new_state, new_token); + emit_change_to_subscribers(*this); +} + +void User::request_log_out() +{ + if (auto app = this->app()) { + auto new_state = is_anonymous() ? SyncUser::State::Removed : SyncUser::State::LoggedOut; + app->m_metadata_store->log_out(m_user_id, new_state); + update_backing_data(app->m_metadata_store->get_user(m_user_id)); + } +} + +void User::request_refresh_user(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + app->get_profile(shared_from_this(), [completion = std::move(completion)](auto, auto error) { + completion(std::move(error)); + }); + } +} + +void User::request_refresh_location(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + bool update_location = true; + app->refresh_access_token(shared_from_this(), update_location, std::move(completion)); + } +} + +void User::request_access_token(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + bool update_location = false; + app->refresh_access_token(shared_from_this(), update_location, std::move(completion)); + } +} + +void User::track_realm(std::string_view path) +{ + if (auto app = this->app()) { + app->m_metadata_store->add_realm_path(m_user_id, path); + } +} + +std::string User::create_file_action(SyncFileAction action, std::string_view original_path, + std::optional requested_recovery_dir) +{ + if (auto app = this->app()) { + std::string recovery_path; + if (action == SyncFileAction::BackUpThenDeleteRealm) { + recovery_path = + util::reserve_unique_file_name(app->m_file_manager->recovery_directory_path(requested_recovery_dir), + util::create_timestamped_template("recovered_realm")); + } + app->m_metadata_store->create_file_action(action, original_path, recovery_path); + return recovery_path; + } + return ""; +} + +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) +{ + if (auto app = this->app()) { + app->refresh_custom_data(shared_from_this(), update_location, std::move(completion_block)); + } + else { + completion_block(app::AppError( + ErrorCodes::ClientUserNotFound, + util::format("Cannot initiate a refresh on user '%1' because the user has been removed", m_user_id))); + } +} + +std::string User::path_for_realm(const SyncConfig& config, std::optional custom_file_name) const +{ + if (auto app = this->app()) { + return app->m_file_manager->path_for_realm(config, std::move(custom_file_name)); + } + return ""; +} +} // 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..7b822675493 --- /dev/null +++ b/src/realm/object-store/sync/app_user.hpp @@ -0,0 +1,267 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 { +struct SyncConfig; +} + +namespace realm::app { +class App; +struct AppError; +class MetadataStore; +class MongoClient; + +struct UserProfile { + // The full name of the user. + std::optional name() const + { + return get_field("name"); + } + // The email address of the user. + std::optional email() const + { + return get_field("email"); + } + // A URL to the user's profile picture. + std::optional picture_url() const + { + return get_field("picture_url"); + } + // The first name of the user. + std::optional first_name() const + { + return get_field("first_name"); + } + // The last name of the user. + std::optional last_name() const + { + return get_field("last_name"); + } + // The gender of the user. + std::optional gender() const + { + return get_field("gender"); + } + // The birthdate of the user. + std::optional birthday() const + { + return get_field("birthday"); + } + // The minimum age of the user. + std::optional min_age() const + { + return get_field("min_age"); + } + // The maximum age of the user. + std::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; + + std::optional get_field(const char* name) const + { + if (auto val = m_data.find(name)) { + return static_cast((*val)); + } + return util::none; + } +}; + +// 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 { + // 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; + // UUIDs which used to be used to generate local Realm file paths. Now only + // used to locate existing files. + std::vector legacy_identities; + // 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 {}; + +public: + // ------------------------------------------------------------------------ + // SyncUser implementation + + std::string user_id() const noexcept override; + std::string app_id() const noexcept override; + std::vector legacy_identities() const override REQUIRES(!m_mutex); + + std::string access_token() const override REQUIRES(!m_mutex); + std::string refresh_token() const override REQUIRES(!m_mutex); + SyncUser::State state() const override 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 override REQUIRES(!m_mutex); + + SyncManager* sync_manager() override REQUIRES(!m_mutex); + void request_log_out() override REQUIRES(!m_mutex); + void request_refresh_user(util::UniqueFunction)>&&) override REQUIRES(!m_mutex); + void request_refresh_location(util::UniqueFunction)>&&) override + REQUIRES(!m_mutex); + void request_access_token(util::UniqueFunction)>&&) override REQUIRES(!m_mutex); + + void track_realm(std::string_view path) override REQUIRES(!m_mutex); + std::string create_file_action(SyncFileAction action, std::string_view original_path, + std::optional requested_recovery_dir) override REQUIRES(!m_mutex); + + // ------------------------------------------------------------------------ + // SDK public API + + /// 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. + std::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); + + // Log the user out and mark it as such. This will also close its associated Sessions. + void log_out() REQUIRES(!m_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, + std::optional custom_file_name = std::nullopt) const 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); + ~User(); + + void update_backing_data(std::optional&& data) REQUIRES(!m_mutex); + void update_data_for_testing(util::FunctionRef) REQUIRES(!m_mutex); + void detach_and_tear_down() 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); + + // Hook for testing access token timeouts + void set_seconds_to_adjust_time_for_testing(int seconds) + { + m_seconds_to_adjust_time_for_testing.store(seconds); + } + +private: + util::CheckedMutex m_mutex; + std::shared_ptr m_app GUARDED_BY(m_mutex); + const std::string m_app_id; + const std::string m_user_id; + UserData m_data GUARDED_BY(m_mutex); + std::atomic m_seconds_to_adjust_time_for_testing = 0; + + bool do_is_anonymous() const REQUIRES(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 8ed703426ea..10ea4eb6ef8 100644 --- a/src/realm/object-store/sync/app_utils.cpp +++ b/src/realm/object-store/sync/app_utils.cpp @@ -79,7 +79,7 @@ std::optional AppUtils::check_for_errors(const Response& response) try { auto ct = find_header("content-type", response.headers); - if (ct && ct->second == "application/json") { + if (ct && ct->second == "application/json" && !response.body.empty()) { auto body = nlohmann::json::parse(response.body); auto message = body.find("error"); auto link = body.find("link"); 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..daf66be89ab --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.cpp @@ -0,0 +1,926 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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; +using realm::app::UserData; + +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 = SyncUser::State; + 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; + } +} + +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 = std::move(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_user_identity_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 obj : *table) { + if (static_cast(obj.get(schema.state_col)) == SyncUser::State::Removed) { + 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); + bool any_failed = false; + for (auto path : paths) { + if (!file_manager.remove_realm(path)) + any_failed = true; + } + try { + file_manager.remove_user_realms(obj.get(m_user_schema.user_id_col)); + } + catch (FileAccessError const&) { + any_failed = true; + } + + // Only remove the object if all of the tracked realms no longer exist, + // and otherwise try again to delete them on the next launch + if (!any_failed) { + 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 && 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 obj : *table) { + if (perform_file_action(file_manager, obj)) + obj.remove(); + } + } + + 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_logged_in_user(std::string_view user_id) override + { + auto realm = get_realm(); + auto obj = find_user(*realm, user_id); + return is_valid_user(obj); + } + + std::optional 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)SyncUser::State::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 UserData& 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(data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut)); + obj.set(schema.refresh_token_col, data.refresh_token.token); + obj.set(schema.access_token_col, data.access_token.token); + obj.set(schema.device_id_col, data.device_id); + + std::stringstream profile; + profile << 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 : 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; + } + + std::optional read_user(const Obj& obj) const + { + if (!obj) { + return {}; + } + auto state = SyncUser::State(obj.get(m_user_schema.state_col)); + if (state == SyncUser::State::Removed) { + return {}; + } + + UserData data; + if (state == SyncUser::State::LoggedIn) { + try { + data.access_token = RealmJWT(get_string(obj, m_user_schema.access_token_col)); + data.refresh_token = RealmJWT(get_string(obj, m_user_schema.refresh_token_col)); + } + catch (...) { + // Invalid stored token results in a logged-out user + data.access_token = {}; + data.refresh_token = {}; + } + } + + data.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.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.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.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.legacy_identities.reserve(legacy_identities.size()); + for (size_t i = 0, size = legacy_identities.size(); i < size; ++i) { + data.legacy_identities.push_back(legacy_identities.get(i)); + } + + return data; + } + + void update_current_user(Realm& realm, std::string_view removed_user_id) + { + auto current_user = current_user_obj(realm); + if (current_user.get(m_current_user_schema.user_id) == removed_user_id) { + // Set to either empty or the first still logged in user + current_user.set(m_current_user_schema.user_id, get_current_user()); + } + } + + void log_out(std::string_view user_id, SyncUser::State new_state) override + { + REALM_ASSERT(new_state != SyncUser::State::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, ""); + update_current_user(*realm, 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); // also removes obj + update_current_user(*realm, user_id); + } + realm->commit_transaction(); + } + + void add_realm_path(std::string_view user_id, std::string_view path) override + { + auto realm = get_realm(); + realm->begin_transaction(); + if (auto obj = find_user(*realm, user_id)) { + obj.get_set(m_user_schema.realm_file_paths_col).insert(path); + } + realm->commit_transaction(); + } + + bool is_valid_user(Obj& obj) + { + // This is overly cautious and merely checking the state should suffice, + // but because this is a persisted file that can be modified it's possible + // to get invalid combinations of data. + return obj && obj.get(m_user_schema.state_col) == int64_t(SyncUser::State::LoggedIn) && + RealmJWT::validate(get_string(obj, m_user_schema.access_token_col)) && + RealmJWT::validate(get_string(obj, m_user_schema.refresh_token_col)); + } + + std::vector get_all_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 (obj.get(m_user_schema.state_col) != int64_t(SyncUser::State::Removed)) { + users.emplace_back(obj.get(m_user_schema.user_id_col)); + } + } + return users; + } + + std::string get_current_user() override + { + auto realm = get_realm(); + if (auto obj = current_user_obj(*realm)) { + auto user_id = obj.get(m_current_user_schema.user_id); + auto user_obj = find_user(*realm, user_id); + if (is_valid_user(user_obj)) { + return 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) override + { + REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !recovery_path.empty()); + + 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)); + // There's also partition and user_id fields in the schema, but they + // aren't actually used for anything and are never read + realm->commit_transaction(); + } + + Obj find_user(Realm& realm, StringData user_id) const + { + Obj obj; + if (user_id.size() == 0) + return obj; + + 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 + if (auto key = q.find()) + obj = table->get_object(key); + return obj; + } +}; + +class InMemoryMetadataStorage : public app::MetadataStore { + std::mutex m_mutex; + std::map> m_users; + std::map, std::less<>> m_realm_paths; + std::string m_active_user; + struct FileAction { + SyncFileAction action; + std::string backup_path; + }; + std::map> m_file_actions; + + bool has_logged_in_user(std::string_view user_id) override + { + std::lock_guard lock(m_mutex); + auto it = m_users.find(user_id); + return it != m_users.end() && it->second.access_token; + } + + std::optional get_user(std::string_view user_id) override + { + std::lock_guard lock(m_mutex); + if (auto it = m_users.find(user_id); it != m_users.end()) { + return it->second; + } + return {}; + } + + void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token, + std::string_view device_id) override + { + std::lock_guard lock(m_mutex); + auto it = m_users.find(user_id); + if (it == m_users.end()) { + it = m_users.insert({std::string(user_id), UserData{}}).first; + m_active_user = user_id; + } + auto& user = it->second; + user.device_id = device_id; + try { + user.refresh_token = RealmJWT(refresh_token); + user.access_token = RealmJWT(access_token); + } + catch (...) { + user.refresh_token = {}; + user.access_token = {}; + } + } + + void update_user(std::string_view user_id, const UserData& data) override + { + std::lock_guard lock(m_mutex); + auto& user = m_users.find(user_id)->second; + user = data; + user.legacy_identities.clear(); + } + + void log_out(std::string_view user_id, SyncUser::State new_state) override + { + std::lock_guard lock(m_mutex); + if (auto it = m_users.find(user_id); it != m_users.end()) { + if (new_state == SyncUser::State::Removed) { + m_users.erase(it); + } + else { + auto& user = it->second; + user.access_token = {}; + user.refresh_token = {}; + user.device_id.clear(); + } + } + } + + void delete_user(SyncFileManager& file_manager, std::string_view user_id) override + { + std::lock_guard lock(m_mutex); + if (auto it = m_users.find(user_id); it != m_users.end()) { + m_users.erase(it); + } + 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 + { + std::lock_guard lock(m_mutex); + if (auto it = m_users.find(m_active_user); it != m_users.end() && it->second.access_token) { + return m_active_user; + } + + for (auto& [user_id, data] : m_users) { + if (data.access_token) { + m_active_user = user_id; + return user_id; + } + } + + return ""; + } + + void set_current_user(std::string_view user_id) override + { + std::lock_guard lock(m_mutex); + m_active_user = user_id; + } + + std::vector get_all_users() override + { + std::lock_guard lock(m_mutex); + 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 + { + std::lock_guard lock(m_mutex); + m_realm_paths[std::string(user_id)].insert(std::string(path)); + } + + bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view path) override + { + std::lock_guard lock(m_mutex); + auto it = m_file_actions.find(path); + if (it == m_file_actions.end()) + return false; + auto& old_path = it->first; + switch (it->second.action) { + case SyncFileAction::DeleteRealm: + if (file_manager.remove_realm(old_path)) { + m_file_actions.erase(it); + return true; + } + return false; + + case SyncFileAction::BackUpThenDeleteRealm: + if (!util::File::exists(old_path)) { + m_file_actions.erase(it); + return true; + } + auto& new_path = it->second.backup_path; + if (!file_manager.copy_realm_file(old_path, new_path)) { + return false; + } + if (file_manager.remove_realm(old_path)) { + m_file_actions.erase(it); + return true; + } + it->second.action = SyncFileAction::DeleteRealm; + return false; + } + return false; + } + + void create_file_action(SyncFileAction action, std::string_view path, std::string_view backup_path) override + { + std::lock_guard lock(m_mutex); + REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !backup_path.empty()); + m_file_actions[std::string(path)] = FileAction{action, std::string(backup_path)}; + } +}; + +} // 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..8972b1406b3 --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.hpp @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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: + 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) = 0; + + virtual bool has_logged_in_user(std::string_view user_id) = 0; + virtual std::optional 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 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, SyncUser::State 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_all_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 84e9a3ecdc2..e022577a50a 100644 --- a/src/realm/object-store/sync/impl/sync_file.cpp +++ b/src/realm/object-store/sync/impl/sync_file.cpp @@ -18,7 +18,11 @@ #include +#include + #include +#include +#include #include #include #include @@ -237,9 +241,10 @@ static std::string validate_and_clean_path(const std::string& path) } // namespace util -SyncFileManager::SyncFileManager(const std::string& base_path, const std::string& app_id) - : m_base_path(util::file_path_by_appending_component(base_path, c_sync_directory, util::FilePathType::Directory)) - , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(app_id), +SyncFileManager::SyncFileManager(const app::AppConfig& config) + : m_base_path(util::file_path_by_appending_component(config.base_file_path, c_sync_directory, + util::FilePathType::Directory)) + , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(config.app_id), util::FilePathType::Directory)) { util::try_make_dir(m_base_path); @@ -253,21 +258,17 @@ 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, - const std::vector& realm_paths) const +void SyncFileManager::remove_user_realms(const std::string& user_id) 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); } @@ -298,11 +299,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); } @@ -331,11 +331,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; } @@ -362,7 +362,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; } @@ -384,12 +384,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; } @@ -397,7 +396,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; @@ -453,12 +452,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)); } @@ -474,14 +472,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; } @@ -507,10 +505,62 @@ 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; +} + } // 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..c5191149e2e 100644 --- a/src/realm/object-store/sync/impl/sync_file.hpp +++ b/src/realm/object-store/sync/impl/sync_file.hpp @@ -19,13 +19,17 @@ #ifndef REALM_OS_SYNC_FILE_HPP #define REALM_OS_SYNC_FILE_HPP +#include #include - -#include - -#include +#include namespace realm { +struct SyncConfig; +class SyncUser; + +namespace app { +struct AppConfig; +} namespace util { @@ -58,27 +62,32 @@ std::string reserve_unique_file_name(const std::string& path, const std::string& // This class manages how Synced Realms are stored on the filesystem. class SyncFileManager { public: - SyncFileManager(const std::string& base_path, const std::string& app_id); + SyncFileManager(const app::AppConfig&); /// Remove the Realms at the specified absolute paths along with any associated helper files. - void remove_user_realms(const std::string& user_identity, - const std::vector& realm_paths) const; // throws + void remove_user_realms(const std::string& user_id) 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; + /// 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 +113,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 +143,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 81cafc2e97d..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ /dev/null @@ -1,845 +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 -#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_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"; - -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}, // unused and should be removed in v8 - {c_sync_identity, PropertyType::String}, // unused and should be removed in v8 - }}, - {c_sync_current_user_identity, - { - {c_sync_current_user_identity, PropertyType::String}, - }}, - }; -} - -void migrate_to_v7(Realm& old_realm, Realm& 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(); - } -} - -void migrate_to_v8(Realm&, Realm& realm) -{ - if (auto app_metadata_table = realm.read_group().get_table("class_AppMetadata")) { - realm.read_group().remove_table(app_metadata_table->get_key()); - } -} - -} // 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); - } - // note that the schema version has not yet been bumped to 8 - if (old_realm->schema_version() < 8) { - migrate_to_v8(*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, - }; -} - -void SyncMetadataManager::perform_launch_actions(SyncFileManager& file_manager) const -{ - auto realm = get_realm(); - - // Perform our "on next startup" actions such as deleting Realm files - // which we couldn't delete immediately due to them being in use - auto actions_table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - for (auto file_action : *actions_table) { - SyncFileActionMetadata md(m_file_action_schema, realm, file_action); - run_file_action(file_manager, md); - } - - // Delete any users marked for death. - auto users_table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - for (auto user : *users_table) { - if (user.get(m_user_schema.state_col) != int64_t(SyncUser::State::Removed)) - continue; - try { - SyncUserMetadata data(m_user_schema, realm, user); - file_manager.remove_user_realms(data.identity(), data.realm_file_paths()); - realm->begin_transaction(); - user.remove(); - realm->commit_transaction(); - } - catch (FileAccessError const&) { - continue; - } - } -} - -bool SyncMetadataManager::run_file_action(SyncFileManager& file_manager, SyncFileActionMetadata& md) const -{ - switch (md.action()) { - case SyncFileActionMetadata::Action::DeleteRealm: - // Delete all the files for the given Realm. - if (file_manager.remove_realm(md.original_name())) { - md.remove(); - return true; - } - break; - 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. - md.remove(); - return false; - } - 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) { - md.remove(); - return true; - } - md.set_action(SyncFileActionMetadata::Action::DeleteRealm); - } - break; - } - return false; -} - -// 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_view get_string(const Obj& obj, ColKey col) -{ - auto str = obj.get(col); - return str.is_null() ? "" : std::string_view(str); -} - -static bool is_valid_user(const SyncUserMetadata::Schema& schema, const Obj& obj) -{ - // This is overly cautious and merely checking the state should suffice, - // but because this is a persisted file that can be modified it's possible - // to get invalid combinations of data. - return obj && obj.get(schema.state_col) == int64_t(SyncUser::State::LoggedIn) && - RealmJWT::validate(get_string(obj, schema.access_token_col)) && - RealmJWT::validate(get_string(obj, schema.refresh_token_col)); -} - -std::vector SyncMetadataManager::all_logged_in_users() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - std::vector users; - users.reserve(table->size()); - for (auto obj : *table) { - if (is_valid_user(m_user_schema, obj)) { - users.emplace_back(m_user_schema, realm, obj); - } - } - return users; -} - -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, SyncFileActionMetadata::Action action, - StringData new_name) const -{ - auto realm = get_realm(); - realm->begin_transaction(); - TableRef table = ObjectStore::table_for_object_type(realm->read_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)); - realm->commit_transaction(); -} - -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)); -} - -bool SyncMetadataManager::perform_file_actions(SyncFileManager& file_manager, StringData path) const -{ - if (auto md = get_file_action_metadata(path)) { - return run_file_action(file_manager, *md); - } - return false; -} - -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 -} - -// 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); -} - -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.emplace_back(obj.get(c_sync_user_id), obj.get(c_sync_provider_type)); - } - - 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)); -} - -SyncFileActionMetadata::Action SyncFileActionMetadata::action() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return static_cast(m_obj.get(m_schema.idx_action)); -} - -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 4987c9e809f..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.hpp +++ /dev/null @@ -1,241 +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 - -namespace realm { -class SyncFileManager; -class SyncMetadataManager; - -// 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; - }; - - 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; - - Action action() 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: - std::vector all_logged_in_users() const; - - // Perform all pending file actions and delete any remaining data for removed users. - void perform_launch_actions(SyncFileManager& file_manager) const; - - // 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; - // Perform any stored file actions for the given path. - bool perform_file_actions(SyncFileManager& file_manager, StringData path) const; - - // Create file action metadata. - void make_file_action_metadata(StringData original_name, SyncFileActionMetadata::Action action, - StringData new_name = {}) const; - - util::Optional get_current_user_identity() const; - void set_current_user_identity(const std::string& identity); - - /// 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; - - 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); - - bool run_file_action(SyncFileManager& file_manager, SyncFileActionMetadata& md) const; -}; - -} // 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 67ce0d08f21..7c3a1a11ce0 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 e9056f30b76..0884459abc6 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 24713d85c42..fcaa336ce8f 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,68 +43,23 @@ SyncClientTimeouts::SyncClientTimeouts() { } -std::shared_ptr SyncManager::create(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id) +std::shared_ptr SyncManager::create(const SyncClientConfig& config) { - return std::make_shared(Private(), std::move(app), std::move(sync_route), config, app_id); + return std::make_shared(Private(), config); } -SyncManager::SyncManager(Private, std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id) +SyncManager::SyncManager(Private, const SyncClientConfig& config) : m_config(config) - , m_file_manager(std::make_unique(m_config.base_file_path, app_id)) - , m_sync_route(sync_route) - , m_app(app) - , m_app_id(app_id) { // create the initial logger - if the logger_factory is updated later, a new // logger will be created at that time. do_make_logger(); - - if (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); - - m_metadata_manager->perform_launch_actions(*m_file_manager); - - // Load persisted users into the users map. - for (auto user : m_metadata_manager->all_logged_in_users()) { - m_users.push_back(std::make_shared(SyncUser::Private(), user, this)); - } -} - -bool SyncManager::immediately_run_file_actions(const std::string& realm_path) -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (m_metadata_manager) { - return m_metadata_manager->perform_file_actions(*m_file_manager, realm_path); - } - return false; } void SyncManager::tear_down_for_testing() { close_all_sessions(); - { - 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. @@ -148,16 +103,9 @@ void SyncManager::tear_down_for_testing() { util::CheckedLockGuard lock(m_mutex); // Destroy the client now that we have no remaining sessions. - m_sync_client = nullptr; + m_sync_client.reset(); 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(); } } @@ -192,6 +140,7 @@ void SyncManager::do_make_logger() else { m_logger_ptr = util::Logger::get_default_logger(); } + REALM_ASSERT(m_logger_ptr); } const std::shared_ptr& SyncManager::get_logger() const @@ -226,169 +175,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 @@ -404,13 +190,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. @@ -419,89 +198,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; } @@ -544,7 +260,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(); } @@ -553,15 +268,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() @@ -623,6 +330,30 @@ void SyncManager::unregister_session(const std::string& path) lock.unlock(); } +void SyncManager::update_sessions_for(SyncUser& user, SyncUser::State old_state, SyncUser::State new_state, + std::string_view new_access_token) +{ + bool should_revive = old_state != SyncUser::State::LoggedIn && new_state == SyncUser::State::LoggedIn; + bool should_stop = old_state == SyncUser::State::LoggedIn && new_state != SyncUser::State::LoggedIn; + + auto sessions = get_all_sessions_for(user); + if (new_access_token.size()) { + for (auto& session : sessions) { + session->update_access_token(new_access_token); + } + } + else if (should_revive) { + for (auto& session : sessions) { + session->revive_if_needed(); + } + } + else if (should_stop) { + for (auto& session : sessions) { + session->force_close(); + } + } +} + void SyncManager::set_session_multiplexing(bool allowed) { util::CheckedLockGuard lock(m_mutex); @@ -646,12 +377,13 @@ SyncClient& SyncManager::get_sync_client() const std::unique_ptr SyncManager::create_sync_client() const { + REALM_ASSERT(m_logger_ptr); return std::make_unique(m_logger_ptr, m_config, weak_from_this()); } void SyncManager::close_all_sessions() { - // log_out() will call unregister_session(), which requires m_session_mutex, + // force_close() will call unregister_session(), which requires m_session_mutex, // so we need to iterate over them without holding the lock. decltype(m_sessions) sessions; { diff --git a/src/realm/object-store/sync/sync_manager.hpp b/src/realm/object-store/sync/sync_manager.hpp index 12e345a93f6..a0d5f63325b 100644 --- a/src/realm/object-store/sync/sync_manager.hpp +++ b/src/realm/object-store/sync/sync_manager.hpp @@ -19,17 +19,10 @@ #ifndef REALM_OS_SYNC_MANAGER_HPP #define REALM_OS_SYNC_MANAGER_HPP -#include - +#include +#include #include -#include -#include -#include -#include -#include - -#include -#include + #include class TestAppSession; @@ -39,87 +32,21 @@ namespace realm { class DB; struct SyncConfig; +struct RealmConfig; class SyncSession; -class SyncUser; -class SyncFileManager; -class SyncMetadataManager; -class SyncFileActionMetadata; 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 { struct Private {}; 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 - // disabled afterwards, and currently is incompatible with automatic failover. + // This must be called before any sync sessions are created and cannot be + // disabled afterwards. void set_session_multiplexing(bool allowed) REQUIRES(!m_mutex); // Destroys the sync manager, terminates all sessions created by it, and stops its SyncClient. @@ -154,6 +81,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); @@ -171,57 +100,20 @@ 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); - // DO NOT CALL OUTSIDE OF TESTING CODE. // Forcibly close all remaining sync sessions, stop the sync client, and // discard all state. The SyncManager must never be used again after this - // function has been called. - void tear_down_for_testing() REQUIRES(!m_mutex, !m_file_system_mutex, !m_user_mutex, !m_session_mutex); + // function has been called (note: not after it has returned). + void tear_down_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); + // Update all sessions for a given user following a state change for that + // user (and optionally a new access token) + void update_sessions_for(SyncUser& user, SyncUser::State old_state, SyncUser::State new_state, + std::string_view new_access_token) REQUIRES(!m_mutex, !m_session_mutex); + // Used by App to update the sync route any time the location info has been refreshed. // m_sync_route starts out as unset when the SyncManager is created or configured. // It will be updated to a valid value upon the first App AppServices HTTP request or @@ -238,18 +130,7 @@ class SyncManager : public std::enable_shared_from_this { const std::optional sync_route() const REQUIRES(!m_mutex) { util::CheckedLockGuard lock(m_mutex); - return m_sync_route; - } - - std::weak_ptr app() const REQUIRES(!m_mutex) - { - util::CheckedLockGuard lock(m_mutex); - return m_app; - } - - const std::string& app_id() const - { - return m_app_id; + return m_sync_route.empty() ? std::nullopt : std::make_optional(m_sync_route); } SyncClientConfig config() const REQUIRES(!m_mutex) @@ -267,10 +148,12 @@ class SyncManager : public std::enable_shared_from_this { static void voluntary_disconnect_all_connections(SyncManager&); }; - static std::shared_ptr create(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id); - SyncManager(Private, std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id); + static std::shared_ptr create(const SyncClientConfig& config); + SyncManager(Private, const SyncClientConfig& config); + + // 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: friend class app::App; @@ -279,6 +162,18 @@ class SyncManager : public std::enable_shared_from_this { friend class ::TestSyncManager; friend class ::TestAppSession; + 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); + + // 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 // due to someone holding a strong reference to it. @@ -289,51 +184,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); - - util::CheckedMutex m_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 - 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 - 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 - 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 `tear_down_for_testing()`. - std::optional m_sync_route GUARDED_BY(m_mutex); - - std::weak_ptr m_app GUARDED_BY(m_mutex); - const std::string m_app_id; }; } // namespace realm diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index c20ec940633..4911adedd19 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(); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -315,10 +315,7 @@ SyncSession::handle_refresh(const std::shared_ptr& session, bool re session->cancel_pending_waits(std::move(lock), refresh_error); } else if (error) { - if (error->code() == ErrorCodes::ClientAppDeallocated) { - return; // this response came in after the app shut down, ignore it - } - else if (!session->get_sync_route()) { + if (!session->get_sync_route()) { // If the sync route is empty at this point, it means the forced location update // failed while trying to start a sync session with a cached user and no other // AppServices HTTP requests have been performed since the App was created. @@ -425,11 +422,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() @@ -443,21 +439,15 @@ void SyncSession::update_error_and_mark_file_for_deletion(SyncError& error, Shou { util::CheckedLockGuard config_lock(m_config_mutex); // Add a SyncFileActionMetadata marking the Realm as needing to be deleted. - std::string recovery_path; auto original_path = path(); error.user_info[SyncError::c_original_file_path_key] = original_path; + using Action = SyncFileAction; + auto action = should_backup == ShouldBackup::yes ? Action::BackUpThenDeleteRealm : Action::DeleteRealm; + std::string recovery_path = m_config.sync_config->user->create_file_action( + action, original_path, m_config.sync_config->recovery_directory); if (should_backup == ShouldBackup::yes) { - recovery_path = util::reserve_unique_file_name( - m_sync_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; - 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)](const auto& manager) { - manager.make_file_action_metadata(original_path, action, recovery_path); - }); } void SyncSession::download_fresh_realm(sync::ProtocolErrorInfo::Action server_requests_action) @@ -762,16 +752,14 @@ 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)); - return; + u->request_access_token(handle_refresh(shared_from_this(), false)); } - break; + return; case sync::ProtocolErrorInfo::Action::RefreshLocation: if (auto u = user()) { - u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); - return; + u->request_refresh_location(handle_refresh(shared_from_this(), true)); } - break; + return; case sync::ProtocolErrorInfo::Action::LogOutUser: next_state = NextStateAfterError::inactive; log_out_user = true; @@ -827,7 +815,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) if (log_out_user) { if (auto u = user()) - u->log_out(); + u->request_log_out(); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -932,7 +920,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; @@ -1215,23 +1203,30 @@ void SyncSession::shutdown_and_wait() m_client.wait_for_session_terminations(); } -void SyncSession::update_access_token(const std::string& signed_token) +void SyncSession::update_access_token(std::string_view signed_token) { util::CheckedUniqueLock lock(m_state_mutex); - // We don't expect there to be a session when waiting for access token, but if there is, refresh its token. - // If not, the latest token will be seeded from SyncUser::access_token() on session creation. - if (m_session) { - m_session->refresh(signed_token); - } - if (m_state == State::WaitingForAccessToken) { - become_active(); + switch (m_state) { + case State::Active: + m_session->refresh(signed_token); + break; + case State::WaitingForAccessToken: + become_active(); + break; + case State::Paused: + // token will be pulled from user when the session is unpaused + return; + case State::Dying: + case State::Inactive: + do_revive(std::move(lock)); + break; } } 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 ceed4dac480..64d50c7ade0 100644 --- a/src/realm/object-store/sync/sync_session.hpp +++ b/src/realm/object-store/sync/sync_session.hpp @@ -93,9 +93,7 @@ class SyncProgressNotifier { // Will be `none` until we've received the initial notification from sync. Note that this // happens only once ever during the lifetime of a given `SyncSession`, since these values are // expected to semi-monotonically increase, and a lower-bounds estimate is still useful in the - // event more up-to-date information isn't yet available. FIXME: If we support transparent - // client reset in the future, we might need to reset the progress state variables if the Realm - // is rolled back. + // event more up-to-date information isn't yet available. util::Optional m_current_progress; std::unordered_map m_packages; @@ -232,7 +230,7 @@ class SyncSession : public std::enable_shared_from_this { // The access token needs to periodically be refreshed and this is how to // let the sync session know to update it's internal copy. - void update_access_token(const std::string& signed_token) REQUIRES(!m_state_mutex, !m_config_mutex); + void update_access_token(std::string_view signed_token) REQUIRES(!m_state_mutex, !m_config_mutex); // Request an updated access token from this session's sync user. void initiate_access_token_refresh() REQUIRES(!m_config_mutex); @@ -360,11 +358,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 deleted file mode 100644 index edac5583a6d..00000000000 --- a/src/realm/object-store/sync/sync_user.cpp +++ /dev/null @@ -1,424 +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 - -namespace realm { - -SyncUserIdentity::SyncUserIdentity(const std::string& id, const std::string& provider_type) - : id(id) - , provider_type(provider_type) -{ -} - -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) -{ - 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) -{ - REALM_ASSERT(m_state == State::LoggedIn && !m_access_token.token.empty() && !m_refresh_token.token.empty()); -} - -std::shared_ptr SyncUser::sync_manager() const -{ - 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(); -} - -void SyncUser::detach_from_sync_manager() -{ - util::CheckedLockGuard lk(m_mutex); - REALM_ASSERT(m_sync_manager); - m_state = SyncUser::State::Removed; - m_sync_manager = nullptr; -} - -std::vector> SyncUser::all_sessions() -{ - 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; - } - // This session is bad, destroy it. - it = m_sessions.erase(it); - } - 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); - } - return locked; -} - -void SyncUser::log_in(const std::string& access_token, const std::string& refresh_token) -{ - 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(); - } - - emit_change_to_subscribers(*this); -} - -void SyncUser::invalidate() -{ - { - 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, "", ""); - }); - } - 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)); - } - } - 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, "", ""); - }); - } - 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); - } - } - 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; -} - -std::string SyncUser::refresh_token() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_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; -} - -bool SyncUser::has_device_id() const -{ - util::CheckedLockGuard lock(m_mutex); - return !m_device_id.empty() && m_device_id != "000000000000000000000000"; -} - -SyncUser::State SyncUser::state() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_state; -} - -SyncUserProfile SyncUser::user_profile() const -{ - 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; -} - -void SyncUser::update_user_profile(std::vector identities, SyncUserProfile profile) -{ - util::CheckedLockGuard lock(m_mutex); - if (m_state == SyncUser::State::Removed) { - return; - } - - 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) -{ - 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; - } -} - -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) -{ - refresh_custom_data(false, std::move(completion_block)); -} - -void SyncUser::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_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); - }); - } -} - -bool SyncUser::access_token_refresh_required() const -{ - 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); -} - -} // 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 56ce45610d0..a229392e719 100644 --- a/src/realm/object-store/sync/sync_user.hpp +++ b/src/realm/object-store/sync/sync_user.hpp @@ -20,281 +20,82 @@ #define REALM_OS_SYNC_USER_HPP #include -#include #include -#include +#include #include -#include #include -#include #include namespace realm { namespace app { +class App; struct AppError; -class MongoClient; } // namespace app class SyncManager; class SyncSession; -class SyncUserMetadata; -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 - { - if (auto val = m_data.find(name)) { - return static_cast((*val)); - } - return util::none; - } +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 }; -// 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 +class SyncUser { +public: + virtual ~SyncUser() = default; + bool is_logged_in() const { - return id != other.id || provider_type != other.provider_type; + return state() == State::LoggedIn; } -}; - -// 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 {}; -public: enum class State { - LoggedOut, - LoggedIn, - Removed, + // changing these is a file-format breaking change + LoggedOut = 0, + LoggedIn = 1, + Removed = 2, }; - // 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); - - /// Returns true if the user's only identity is anonymous. - bool is_anonymous() const REQUIRES(!m_mutex, !m_tokens_mutex); - - const std::string& identity() const noexcept - { - return m_identity; - } - - const std::vector& legacy_identities() const noexcept + /// Server-supplied unique id for this user. + virtual std::string user_id() const noexcept = 0; + /// App id which this user is associated with + virtual std::string app_id() const noexcept = 0; + /// Legacy uuids attached to this user. Only applicable to app::User. + virtual std::vector legacy_identities() const { - return m_legacy_identities; + return {}; } - 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); - - // 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; - - // 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); + virtual std::string access_token() const = 0; + virtual std::string refresh_token() const = 0; + virtual State state() const = 0; /// 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); - - // Hook for testing access token timeouts - void set_seconds_to_adjust_time_for_testing(int seconds) - { - m_seconds_to_adjust_time_for_testing.store(seconds); - } - -protected: - friend class SyncManager; - void detach_from_sync_manager() 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; - - 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; - - 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); - - const std::string m_device_id; - - SyncManager* m_sync_manager; - - std::atomic m_seconds_to_adjust_time_for_testing = 0; + virtual bool access_token_refresh_required() const = 0; + + virtual SyncManager* sync_manager() = 0; + + using CompletionHandler = util::UniqueFunction)>; + // The sync server has told the client to log out the user + // No completion handler as the user is already logged out server-side + virtual void request_log_out() = 0; + // The sync server has told the client to refresh the user's profile + virtual void request_refresh_user(CompletionHandler&&) = 0; + // The sync server has told the client to refresh the user's location + virtual void request_refresh_location(CompletionHandler&&) = 0; + // The sync server has told the client to refresh the user's access token + virtual void request_access_token(CompletionHandler&&) = 0; + + // Called whenever a Realm is opened with this user to enable deleting them + // when the user is removed + virtual void track_realm(std::string_view path) = 0; + // if the action is BackUpThenDeleteRealm, the path where it was backed up is returned + virtual std::string create_file_action(SyncFileAction action, std::string_view original_path, + std::optional requested_recovery_dir) = 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/sync/client.cpp b/src/realm/sync/client.cpp index 703808c87f1..1a54934a1eb 100644 --- a/src/realm/sync/client.cpp +++ b/src/realm/sync/client.cpp @@ -110,7 +110,7 @@ class SessionWrapper final : public util::AtomicRefCountBase, DB::CommitListener bool wait_for_upload_complete_or_client_stopped(); bool wait_for_download_complete_or_client_stopped(); - void refresh(std::string signed_access_token); + void refresh(std::string_view signed_access_token); static void abandon(util::bind_ptr) noexcept; @@ -1501,13 +1501,13 @@ bool SessionWrapper::wait_for_download_complete_or_client_stopped() } -void SessionWrapper::refresh(std::string signed_access_token) +void SessionWrapper::refresh(std::string_view signed_access_token) { // Thread safety required REALM_ASSERT(m_initiated); REALM_ASSERT(!m_abandoned); - m_client.post([self = util::bind_ptr(this), token = std::move(signed_access_token)](Status status) { + m_client.post([self = util::bind_ptr(this), token = std::string(signed_access_token)](Status status) { if (status == ErrorCodes::OperationAborted) return; else if (!status.is_ok()) @@ -2135,7 +2135,7 @@ bool Session::wait_for_download_complete_or_client_stopped() } -void Session::refresh(const std::string& signed_access_token) +void Session::refresh(std::string_view signed_access_token) { m_impl->refresh(signed_access_token); // Throws } diff --git a/src/realm/sync/client.hpp b/src/realm/sync/client.hpp index f38f35f3e6b..1e05f6186b1 100644 --- a/src/realm/sync/client.hpp +++ b/src/realm/sync/client.hpp @@ -564,7 +564,7 @@ class Session { /// /// \param signed_user_token A cryptographically signed token describing the /// identity and access rights of the current user. See ProtocolEnvelope. - void refresh(const std::string& signed_user_token); + void refresh(std::string_view signed_user_token); /// \brief Inform the synchronization agent about changes of local origin. /// 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/src/realm/util/file.cpp b/src/realm/util/file.cpp index 5e9e52ebe11..84bbe407c93 100644 --- a/src/realm/util/file.cpp +++ b/src/realm/util/file.cpp @@ -395,19 +395,16 @@ std::string make_temp_file(const char* prefix) char* tmp_dir_env = getenv("TMPDIR"); std::string base_dir = tmp_dir_env ? tmp_dir_env : std::string(P_tmpdir); if (!base_dir.empty() && base_dir[base_dir.length() - 1] != '/') { - base_dir = base_dir + "/"; + base_dir += '/'; } #endif - std::string tmp = base_dir + prefix + std::string("_XXXXXX") + std::string("\0", 1); - std::unique_ptr buffer = std::make_unique(tmp.size()); // Throws - memcpy(buffer.get(), tmp.c_str(), tmp.size()); - char* filename = buffer.get(); - auto fd = mkstemp(filename); + std::string filename = util::format("%1%2_XXXXXX", base_dir, prefix); + auto fd = mkstemp(filename.data()); if (fd == -1) { throw std::system_error(errno, std::system_category(), "mkstemp() failed"); // LCOV_EXCL_LINE } close(fd); - return std::string(filename); + return filename; #endif } diff --git a/src/realm/util/random.hpp b/src/realm/util/random.hpp index 541252ede84..fc826efe162 100644 --- a/src/realm/util/random.hpp +++ b/src/realm/util/random.hpp @@ -22,8 +22,6 @@ namespace util { /// up the engine state. /// /// Thread-safe. -/// -/// FIXME: Move this to core repo, as it is generally useful. template void seed_prng_nondeterministically(Engine&); diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index b23946ec353..8bdbfc92e39 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -71,7 +71,6 @@ if(REALM_ENABLE_SYNC) sync/session/session.cpp sync/session/wait_for_completion.cpp sync/sync_manager.cpp - sync/user.cpp util/sync/sync_test_utils.cpp util/unit_test_transport.cpp ) diff --git a/test/object-store/audit.cpp b/test/object-store/audit.cpp index a0b5ffef2da..d8ea47b4df7 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -174,7 +174,7 @@ void sort_events(std::vector& events) } #if REALM_ENABLE_AUTH_TESTS -static std::vector get_audit_events_from_baas(TestAppSession& session, SyncUser& user, +static std::vector get_audit_events_from_baas(TestAppSession& session, app::User& user, size_t expected_count) { static const std::set nonmetadata_fields = {"activity", "event", "data", "realm_id"}; @@ -306,6 +306,7 @@ TEST_CASE("audit object serialization", "[sync][pbs][audit]") { {"target", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}, {"embedded target", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::Int}}}}; config.audit_config = std::make_shared(); + config.audit_config->base_file_path = test_session.base_file_path(); auto serializer = std::make_shared(); config.audit_config->serializer = serializer; config.audit_config->logger = audit_logger; @@ -1079,6 +1080,7 @@ TEST_CASE("audit management", "[sync][pbs][audit]") { {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}, }; config.audit_config = std::make_shared(); + config.audit_config->base_file_path = test_session.base_file_path(); auto realm = Realm::get_shared_realm(config); auto audit = realm->audit_context(); REQUIRE(audit); @@ -1508,6 +1510,7 @@ TEST_CASE("audit realm sharding", "[sync][pbs][audit]") { {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"value", PropertyType::Int}}}, }; config.audit_config = std::make_shared(); + config.audit_config->base_file_path = test_session.base_file_path(); config.audit_config->logger = audit_logger; auto realm = Realm::get_shared_realm(config); auto audit = realm->audit_context(); @@ -1542,7 +1545,7 @@ TEST_CASE("audit realm sharding", "[sync][pbs][audit]") { // There should now be several unuploaded Realms in the local client // directory - auto root = test_session.base_file_path() + "/realm-audit/app_id/test/audit"; + auto root = test_session.base_file_path() + "/realm-audit/app id/test/audit"; std::string file_name; util::DirScanner dir(root); size_t file_count = 0; @@ -1613,6 +1616,7 @@ TEST_CASE("audit realm sharding", "[sync][pbs][audit]") { SyncTestFile config(test_session, "other"); config.audit_config = std::make_shared(); config.audit_config->logger = audit_logger; + config.audit_config->base_file_path = test_session.base_file_path(); auto realm = Realm::get_shared_realm(config); auto audit2 = realm->audit_context(); REQUIRE(audit2); @@ -1639,6 +1643,7 @@ TEST_CASE("audit realm sharding", "[sync][pbs][audit]") { // Open the same Realm with a different audit prefix SyncTestFile config(test_session, "parent"); config.audit_config = std::make_shared(); + config.audit_config->base_file_path = test_session.base_file_path(); config.audit_config->logger = audit_logger; config.audit_config->partition_value_prefix = "other"; auto realm = Realm::get_shared_realm(config); @@ -1698,6 +1703,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { config.schema = schema; config.audit_config = std::make_shared(); config.audit_config->logger = audit_logger; + config.audit_config->base_file_path = session.app()->config().base_file_path; auto expect_error = [&](auto&& config, auto&& fn) -> SyncError { std::mutex mutex; @@ -1756,6 +1762,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { SyncTestFile config(session_2.app()->current_user(), bson::Bson("default")); config.schema = no_audit_event_schema; config.audit_config = std::make_shared(); + config.audit_config->base_file_path = session.app()->config().base_file_path; config.audit_config->audit_user = audit_user; auto realm = Realm::get_shared_realm(config); @@ -1793,7 +1800,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { auto audit_user = session.app()->current_user(); config.audit_config->audit_user = audit_user; auto realm = Realm::get_shared_realm(config); - session.sync_manager()->remove_user(audit_user->identity()); + session.app()->remove_user(audit_user, nullptr); auto audit = realm->audit_context(); auto scope = audit->begin_scope("scope"); @@ -1815,6 +1822,7 @@ TEST_CASE("audit integration tests", "[sync][pbs][audit][baas]") { SyncTestFile config(session_2.app()->current_user(), bson::Bson("default")); config.schema = no_audit_event_schema; config.audit_config = std::make_shared(); + config.audit_config->base_file_path = session.app()->config().base_file_path; auto error = expect_error(config, generate_event); REQUIRE_THAT(error.status.reason(), StartsWith("Invalid schema change")); diff --git a/test/object-store/benchmarks/client_reset.cpp b/test/object-store/benchmarks/client_reset.cpp index b232eb1cf3d..793eb89c89c 100644 --- a/test/object-store/benchmarks/client_reset.cpp +++ b/test/object-store/benchmarks/client_reset.cpp @@ -22,11 +22,11 @@ #include #include +#include #include #include #include #include - #include #include #include diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 0cb19dde423..04f09bf14c5 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -594,11 +594,10 @@ TEST_CASE("C API (non-database)", "[c_api]") { CHECK(app_config->device_info.bundle_id == "some_bundle_id"); test_util::TestDirGuard temp_dir(util::make_temp_dir()); - auto sync_client_config = cptr(realm_sync_client_config_new()); - realm_sync_client_config_set_base_file_path(sync_client_config.get(), temp_dir.c_str()); - realm_sync_client_config_set_metadata_mode(sync_client_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); + realm_app_config_set_base_file_path(app_config.get(), temp_dir.c_str()); + realm_app_config_set_metadata_mode(app_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); - auto test_app = cptr(realm_app_create(app_config.get(), sync_client_config.get())); + auto test_app = cptr(realm_app_create(app_config.get())); realm_user_t* sync_user; auto user_data_free = [](realm_userdata_t) {}; @@ -5583,7 +5582,7 @@ TEST_CASE("C API - async_open", "[sync][pbs][c_api]") { SECTION("can open synced Realms that don't already exist") { realm_config_t* config = realm_config_new(); config->schema = Schema{object_schema}; - realm_user user(test_config.sync_config->user); + realm_user user(init_sync_manager.fake_user()); realm_sync_config_t* sync_config = realm_sync_config_new(&user, "default"); realm_sync_config_set_initial_subscription_handler(sync_config, task_init_subscription, false, nullptr, nullptr); @@ -5615,32 +5614,28 @@ TEST_CASE("C API - async_open", "[sync][pbs][c_api]") { SECTION("cancels download and reports an error on auth error") { auto expired_token = encode_fake_jwt("", 123, 456); - - struct Transport : UnitTestTransport { - void send_request_to_server( - const realm::app::Request& req, - realm::util::UniqueFunction&& completion) override + struct User : TestUser { + using TestUser::TestUser; + void request_access_token(CompletionHandler&& completion) override { - if (req.url.find("/auth/session") != std::string::npos) { - completion(app::Response{403}); - } - else { - UnitTestTransport::send_request_to_server(req, std::move(completion)); - } + completion(app::AppError(ErrorCodes::HTTPError, "403 error", "", 403)); + } + bool access_token_refresh_required() const override + { + return true; } }; - OfflineAppSession::Config oas_config; - oas_config.transport = std::make_shared(); - OfflineAppSession oas(oas_config); - SyncTestFile test_config(oas, "realm"); - test_config.sync_config->user->log_in(expired_token, expired_token); + auto user = std::make_shared("realm", init_sync_manager.sync_manager()); + user->m_access_token = expired_token; + user->m_refresh_token = expired_token; realm_config_t* config = realm_config_new(); config->schema = Schema{object_schema}; - realm_user user(test_config.sync_config->user); - realm_sync_config_t* sync_config = realm_sync_config_new(&user, "realm"); + realm_user c_user(user); + realm_sync_config_t* sync_config = realm_sync_config_new(&c_user, "realm"); realm_sync_config_set_initial_subscription_handler(sync_config, task_init_subscription, false, nullptr, nullptr); + realm_config_set_path(config, test_config.path.c_str()); realm_config_set_schema_version(config, 1); Userdata userdata; @@ -5656,8 +5651,7 @@ TEST_CASE("C API - async_open", "[sync][pbs][c_api]") { REQUIRE(userdata.called); REQUIRE(!userdata.realm_ref); REQUIRE(userdata.error.error == RLM_ERR_AUTH_ERROR); - REQUIRE(userdata.error_message == - "Unable to refresh the user access token: http error code considered fatal. Client Error: 403"); + REQUIRE(userdata.error_message == "Unable to refresh the user access token: 403 error. Client Error: 403"); realm_release(task); realm_release(config); realm_release(sync_config); @@ -6164,8 +6158,7 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a CHECK(realm_equals(sync_user_1, current_user)); realm_release(current_user); - realm_user_t* sync_user_2; - realm_app_switch_user(&app, sync_user_1, &sync_user_2); + realm_app_switch_user(&app, sync_user_1); size_t out_n = 0; realm_app_get_all_users(&app, nullptr, 0, &out_n); @@ -6180,7 +6173,6 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a for (size_t i = 0; i < out_n; ++i) realm_release(out_users[i]); realm_release(sync_user_1); - realm_release(sync_user_2); } SECTION("realm_app_user_apikey_provider_client_fetch_apikeys") { SECTION("Failure") { diff --git a/test/object-store/realm.cpp b/test/object-store/realm.cpp index 93f10916b31..dfb86dc0dc2 100644 --- a/test/object-store/realm.cpp +++ b/test/object-store/realm.cpp @@ -47,8 +47,9 @@ #include #include -#include +#include #include + #include #include #endif @@ -1137,11 +1138,24 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { auto expired_token = encode_fake_jwt("", 123, 456); SECTION("can async open while waiting for a token refresh") { - SyncTestFile config(tsm, "realm"); - auto user = config.sync_config->user; + struct User : TestUser { + using TestUser::TestUser; + CompletionHandler stored_completion; + void request_access_token(CompletionHandler&& completion) override + { + stored_completion = std::move(completion); + } + bool access_token_refresh_required() const override + { + return !stored_completion; + } + }; + auto user = std::make_shared("realm", tsm.sync_manager()); + SyncTestFile config(user, "realm"); auto valid_token = user->access_token(); - user->update_access_token(std::move(expired_token)); + user->m_access_token = expired_token; + REQUIRE_FALSE(user->stored_completion); std::atomic called{false}; auto task = Realm::get_synchronized_realm(config); task->start([&](auto ref, auto error) { @@ -1150,11 +1164,10 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { REQUIRE(!error); called = true; }); - auto session = tsm.sync_manager()->get_existing_session(config.path); - REQUIRE(session); - CHECK(session->state() == SyncSession::State::WaitingForAccessToken); + REQUIRE(user->stored_completion); + user->m_access_token = valid_token; + user->stored_completion({}); - session->update_access_token(valid_token); util::EventLoop::main().run_until([&] { return called.load(); }); @@ -1163,25 +1176,21 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { } SECTION("cancels download and reports an error on auth error") { - struct Transport : UnitTestTransport { - void send_request_to_server( - const realm::app::Request& req, - realm::util::UniqueFunction&& completion) override + struct User : TestUser { + using TestUser::TestUser; + void request_access_token(CompletionHandler&& completion) override { - if (req.url.find("/auth/session") != std::string::npos) { - completion(app::Response{403}); - } - else { - UnitTestTransport::send_request_to_server(req, std::move(completion)); - } + completion(app::AppError(ErrorCodes::HTTPError, "403 error", "", 403)); + } + bool access_token_refresh_required() const override + { + return true; } }; - OfflineAppSession::Config oas_config; - oas_config.transport = std::make_shared(); - OfflineAppSession oas(oas_config); - - SyncTestFile config(oas, "realm"); - config.sync_config->user->log_in(expired_token, expired_token); + auto user = std::make_shared("realm", tsm.sync_manager()); + user->m_access_token = expired_token; + user->m_refresh_token = expired_token; + SyncTestFile config(user, "realm"); bool got_error = false; config.sync_config->error_handler = [&](std::shared_ptr, SyncError) { @@ -1192,9 +1201,8 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { task->start([&](auto ref, auto error) { std::lock_guard lock(mutex); REQUIRE(error); - REQUIRE_EXCEPTION( - std::rethrow_exception(error), HTTPError, - "Unable to refresh the user access token: http error code considered fatal. Client Error: 403"); + REQUIRE_EXCEPTION(std::rethrow_exception(error), HTTPError, + "Unable to refresh the user access token: 403 error. Client Error: 403"); REQUIRE(!ref); called = true; }); diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index fb976e45955..2219d9f87df 100644 --- a/test/object-store/sync/app.cpp +++ b/test/object-store/sync/app.cpp @@ -21,6 +21,7 @@ #include "util/sync/sync_test_utils.hpp" #include "util/test_path.hpp" #include "util/unit_test_transport.hpp" +#include "util/test_path.hpp" #include #include @@ -64,13 +65,13 @@ using namespace Catch::Matchers; namespace { -std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) +std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) { if (auto transport = dynamic_cast(app->config().transport.get())) { transport->set_provider_type(credentials.provider_as_string()); } - std::shared_ptr user; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user_arg, Optional error) { + std::shared_ptr user; + app->log_in_with_credentials(credentials, [&](std::shared_ptr user_arg, Optional error) { REQUIRE_FALSE(error); REQUIRE(user_arg); user = std::move(user_arg); @@ -82,7 +83,7 @@ std::shared_ptr log_in(std::shared_ptr app, AppCredentials creden AppError failed_log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) { Optional err; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user, Optional error) { + app->log_in_with_credentials(credentials, [&](std::shared_ptr user, Optional error) { REQUIRE(error); REQUIRE_FALSE(user); err = error; @@ -785,7 +786,7 @@ TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][ SECTION("cannot login with wrong password") { app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"), - [&](std::shared_ptr user, Optional error) { + [&](std::shared_ptr user, Optional error) { CHECK(!user); REQUIRE(error); REQUIRE(error->code() == ErrorCodes::InvalidPassword); @@ -922,7 +923,7 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa App::UserAPIKey api_key; SECTION("api-key") { - std::shared_ptr logged_in_user = app->current_user(); + std::shared_ptr logged_in_user = app->current_user(); auto api_key_name = util::format("%1", random_string(15)); client.create_api_key(api_key_name, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { @@ -982,7 +983,7 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa } SECTION("api-key without a user") { - std::shared_ptr no_user = nullptr; + std::shared_ptr no_user = nullptr; auto api_key_name = util::format("%1", random_string(15)); client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional error) { REQUIRE(error); @@ -1048,9 +1049,9 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa } SECTION("api-key against the wrong user") { - std::shared_ptr first_user = app->current_user(); + std::shared_ptr first_user = app->current_user(); create_user_and_log_in(app); - std::shared_ptr second_user = app->current_user(); + std::shared_ptr second_user = app->current_user(); REQUIRE(first_user != second_user); auto api_key_name = util::format("%1", random_string(15)); App::UserAPIKey api_key; @@ -1208,7 +1209,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); - app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { + app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { REQUIRE_FALSE(error); REQUIRE(user == user2); REQUIRE(user->identities().size() == 2); @@ -1218,7 +1219,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { } SECTION("linking an identity makes the user no longer returned by anonymous logins") { - app->link_user(user, creds, [&](std::shared_ptr, Optional error) { + app->link_user(user, creds, [&](std::shared_ptr, Optional error) { REQUIRE_FALSE(error); }); auto user2 = log_in(app); @@ -1226,7 +1227,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { } SECTION("existing users are reused when logging in via linked identities") { - app->link_user(user, creds, [](std::shared_ptr, Optional error) { + app->link_user(user, creds, [](std::shared_ptr, Optional error) { REQUIRE_FALSE(error); }); app->log_out([](auto error) { @@ -1305,7 +1306,7 @@ TEST_CASE("app: delete user with credentials integration", "[sync][app][user][ba CHECK(user->state() == SyncUser::State::Removed); CHECK(app->current_user() == nullptr); - app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { + app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { CHECK(!user); REQUIRE(error); REQUIRE(error->code() == ErrorCodes::InvalidPassword); @@ -2015,7 +2016,7 @@ TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") { TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") { TestAppSession session; auto app = session.app(); - std::shared_ptr sync_user = app->current_user(); + std::shared_ptr sync_user = app->current_user(); SECTION("register") { bool processed; @@ -2097,8 +2098,10 @@ TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") { TEST_CASE("app: token refresh", "[sync][app][token][baas]") { TestAppSession session; auto app = session.app(); - std::shared_ptr sync_user = app->current_user(); - sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token")); + std::shared_ptr sync_user = app->current_user(); + sync_user->update_data_for_testing([](UserData& data) { + data.access_token = RealmJWT(ENCODE_FAKE_JWT("fake_access_token")); + }); auto remote_client = app->current_user()->mongo_client("BackingDB"); auto app_session = get_runtime_app_session(); @@ -2177,7 +2180,7 @@ TEST_CASE("app: mixed lists with object links", "[sync][pbs][app][links][baas]") { TestAppSession test_session(app_session); - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); CHECK(!wait_for_download(*realm)); @@ -2220,7 +2223,7 @@ TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") { auto obj_id = ObjectId::gen(); { TestAppSession test_session(app_session, nullptr, DeleteApp{false}); - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); CppContext c(realm); @@ -2237,7 +2240,7 @@ TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") { { TestAppSession test_session(app_session); - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); CHECK(!wait_for_download(*realm)); @@ -2386,7 +2389,7 @@ TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") { auto dict_obj_id = ObjectId::gen(); { - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); CppContext c(realm); @@ -2446,7 +2449,7 @@ TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") { } { - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); CHECK(!wait_for_download(*realm)); @@ -2489,9 +2492,9 @@ TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") { auto app = session.app(); auto schema = get_default_schema(); - SyncTestFile original_config(app, bson::Bson("foo"), schema); + SyncTestFile original_config(app->current_user(), bson::Bson("foo"), schema); create_user_and_log_in(app); - SyncTestFile target_config(app, bson::Bson("foo"), schema); + SyncTestFile target_config(app->current_user(), bson::Bson("foo"), schema); // Create realm file without client file id { @@ -2630,7 +2633,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Add Objects") { { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(get_dogs(r).size() == 0); @@ -2640,7 +2643,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { { create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto r = Realm::get_shared_realm(config); Results dogs = get_dogs(r); REQUIRE(dogs.size() == 1); @@ -2651,7 +2654,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("MemOnly durability") { { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); config.in_memory = true; config.encryption_key = std::vector(); @@ -2665,7 +2668,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { { create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); config.in_memory = true; config.encryption_key = std::vector(); auto r = Realm::get_shared_realm(config); @@ -2687,21 +2690,22 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } } SynchronousTestTransport::send_request_to_server(request, [&](const Response& response) mutable { + Response modifiable_response = response; if (response_hook) { - response_hook(request, response); + response_hook(request, modifiable_response); } - completion(response); + completion(modifiable_response); }); } // Optional handler for the request and response before it is returned to completion - std::function response_hook; + std::function response_hook; // Optional handler for the request before it is sent to the server std::function(const Request&)> request_hook; }; SECTION("Fast clock on client") { { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(get_dogs(r).size() == 0); @@ -2712,10 +2716,10 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto transport = std::make_shared(); TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); auto app = hooked_session.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); - // Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally + // Make the User behave as if the client clock is 31 minutes fast, so the token looks expired locally // (access tokens have an lifetime of 30 minutes today). user->set_seconds_to_adjust_time_for_testing(31 * 60); REQUIRE(user->access_token_refresh_required()); @@ -2726,7 +2730,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { transport->request_hook = [&](const Request&) -> std::optional { auto user = app->current_user(); REQUIRE(user); - for (auto& session : user->all_sessions()) { + for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) { // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the // WaitingForAccessToken state. if (session->state() == SyncSession::State::WaitingForAccessToken) { @@ -2736,7 +2740,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } return std::nullopt; }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(seen_waiting_for_access_token); Results dogs = get_dogs(r); @@ -2748,8 +2752,8 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Expired Tokens") { sync::AccessToken token; { - std::shared_ptr user = app->current_user(); - SyncTestFile config(app, partition, schema); + std::shared_ptr user = app->current_user(); + SyncTestFile config(user, partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(get_dogs(r).size() == 0); @@ -2770,11 +2774,13 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto transport = std::make_shared(); TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); auto app = hooked_session.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. - user->update_access_token(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); + user->update_data_for_testing([&token](UserData& data) { + data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); + }); REQUIRE(user->access_token_refresh_required()); SECTION("Expired Access Token is Refreshed") { @@ -2784,7 +2790,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { transport->request_hook = [&](const Request&) -> std::optional { auto user = app->current_user(); REQUIRE(user); - for (auto& session : user->all_sessions()) { + for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) { if (session->state() == SyncSession::State::WaitingForAccessToken) { REQUIRE(!seen_waiting_for_access_token); seen_waiting_for_access_token = true; @@ -2792,7 +2798,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } return std::nullopt; }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(seen_waiting_for_access_token); Results dogs = get_dogs(r); @@ -2803,17 +2809,26 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("User is logged out if the refresh request is denied") { REQUIRE(user->is_logged_in()); - transport->response_hook = [&](const Request& request, const Response& response) { + size_t hook_count = 0; + transport->response_hook = [&](const Request& request, Response& response) { auto user = app->current_user(); - REQUIRE(user); + if (hook_count++ == 0) { + // the initial request should have a current user and log it out + REQUIRE(user); + REQUIRE(user->is_logged_in()); + } + else { + INFO(request.url); + // any later requests (eg. redirect) won't have a current user + REQUIRE(!user); + } // simulate the server denying the refresh if (request.url.find("/session") != std::string::npos) { - auto& response_ref = const_cast(response); - response_ref.http_status_code = 401; - response_ref.body = "fake: refresh token could not be refreshed"; + response.http_status_code = 401; + response.body = "fake: refresh token could not be refreshed"; } }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); std::atomic sync_error_handler_called{false}; config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { sync_error_handler_called.store(true); @@ -2835,8 +2850,18 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { user->log_out(); return std::nullopt; }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); + std::atomic sync_error_handler_called{false}; + config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + sync_error_handler_called.store(true); + REQUIRE(error.status.code() == ErrorCodes::AuthError); + REQUIRE_THAT(std::string{error.status.reason()}, + Catch::Matchers::StartsWith("Unable to refresh the user access token")); + }; auto r = Realm::get_shared_realm(config); + timed_wait_for([&] { + return sync_error_handler_called.load(); + }); REQUIRE_FALSE(user->is_logged_in()); REQUIRE(user->state() == SyncUser::State::LoggedOut); } @@ -2847,15 +2872,14 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { std::atomic did_receive_valid_token{false}; constexpr size_t num_error_responses = 6; - transport->response_hook = [&](const Request& request, const Response& response) { + transport->response_hook = [&](const Request& request, Response& response) { // simulate the server experiencing an internal server error if (request.url.find("/session") != std::string::npos) { if (response_times.size() >= num_error_responses) { did_receive_valid_token.store(true); return; } - auto& response_ref = const_cast(response); - response_ref.http_status_code = 500; + response.http_status_code = 500; } }; transport->request_hook = [&](const Request& request) -> std::optional { @@ -2864,7 +2888,12 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } return std::nullopt; }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); + config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + REQUIRE(error.status.code() == ErrorCodes::AuthError); + REQUIRE_THAT(std::string{error.status.reason()}, + Catch::Matchers::StartsWith("Unable to refresh the user access token")); + }; auto r = Realm::get_shared_realm(config); create_one_dog(r); timed_wait_for( @@ -2906,8 +2935,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Invalid refresh token") { auto& app_session = session.app_session(); std::mutex mtx; - auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr user, - Realm::Config config) { + auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr user, Realm::Config config) { REQUIRE(user); REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); @@ -2923,7 +2951,9 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { // instead allowing their session to time out as normal. So this simulates the access token expiring. // see: // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386 - user->update_access_token(encode_fake_jwt("fake_access_token")); + user->update_data_for_testing([](UserData& data) { + data.access_token = RealmJWT(encode_fake_jwt("fake_access_token")); + }); REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); auto [sync_error_promise, sync_error] = util::make_promise_future(); @@ -2936,7 +2966,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto transport = static_cast(session.transport()); transport->block(); // don't let the token refresh happen until we're ready for it auto r = Realm::get_shared_realm(config); - auto session = user->session_for_on_disk_path(config.path); + auto session = app->sync_manager()->get_existing_session(config.path); REQUIRE(user->is_logged_in()); REQUIRE(!sync_error.is_ready()); { @@ -2966,11 +2996,11 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Disabled user results in a sync error") { auto creds = create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); auto user = app->current_user(); REQUIRE(user); + SyncTestFile config(user, partition, schema); REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - app_session.admin_api.disable_user_sessions(app->current_user()->identity(), app_session.server_app_id); + app_session.admin_api.disable_user_sessions(app->current_user()->user_id(), app_session.server_app_id); verify_error_on_sync_with_invalid_refresh_token(user, config); @@ -2979,7 +3009,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { REQUIRE(error.code() == ErrorCodes::UserDisabled); // admin enables user sessions again which should allow the session to continue - app_session.admin_api.enable_user_sessions(user->identity(), app_session.server_app_id); + app_session.admin_api.enable_user_sessions(user->user_id(), app_session.server_app_id); // logging in now works properly log_in(app, creds); @@ -2997,10 +3027,10 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Revoked refresh token results in a sync error") { auto creds = create_user_and_log_in(app); - SyncTestFile config(app, partition, schema); auto user = app->current_user(); + SyncTestFile config(user, partition, schema); REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); - app_session.admin_api.revoke_user_sessions(user->identity(), app_session.server_app_id); + app_session.admin_api.revoke_user_sessions(user->user_id(), app_session.server_app_id); // revoking a user session only affects the refresh token, so the access token should still continue to // work. REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id)); @@ -3031,9 +3061,9 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { app->current_user()->log_out(); auto anon_user = log_in(app); REQUIRE(app->current_user() == anon_user); - SyncTestFile config(app, partition, schema); + SyncTestFile config(anon_user, partition, schema); REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); - app_session.admin_api.revoke_user_sessions(anon_user->identity(), app_session.server_app_id); + app_session.admin_api.revoke_user_sessions(anon_user->user_id(), app_session.server_app_id); // revoking a user session only affects the refresh token, so the access token should still continue to // work. REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id)); @@ -3050,28 +3080,30 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { REQUIRE(error); REQUIRE(error->reason() == util::format("Cannot initiate a refresh on user '%1' because the user has been removed", - anon_user->identity())); + anon_user->user_id())); }); REQUIRE_EXCEPTION( Realm::get_shared_realm(config), ClientUserNotFound, util::format("Cannot start a sync session for user '%1' because this user has been removed.", - anon_user->identity())); + anon_user->user_id())); } SECTION("Opening a Realm with a removed email user results produces an exception") { auto creds = create_user_and_log_in(app); auto email_user = app->current_user(); - const std::string user_ident = email_user->identity(); + const std::string user_ident = email_user->user_id(); REQUIRE(email_user); - SyncTestFile config(app, partition, schema); + SyncTestFile config(email_user, partition, schema); REQUIRE(email_user->is_logged_in()); { // sync works on a valid user auto r = Realm::get_shared_realm(config); Results dogs = get_dogs(r); } - app->sync_manager()->remove_user(user_ident); + app->remove_user(email_user, [](util::Optional err) { + REQUIRE(!err); + }); REQUIRE_FALSE(email_user->is_logged_in()); REQUIRE(email_user->state() == SyncUser::State::Removed); @@ -3081,14 +3113,14 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { util::format("Cannot start a sync session for user '%1' because this user has been removed.", user_ident)); - std::shared_ptr new_user_instance = log_in(app, creds); + std::shared_ptr new_user_instance = log_in(app, creds); // the previous instance is still invalid REQUIRE_FALSE(email_user->is_logged_in()); REQUIRE(email_user->state() == SyncUser::State::Removed); // but the new instance will work and has the same server issued ident REQUIRE(new_user_instance); REQUIRE(new_user_instance->is_logged_in()); - REQUIRE(new_user_instance->identity() == user_ident); + REQUIRE(new_user_instance->user_id() == user_ident); { // sync works again if the same user is logged back in config.sync_config->user = new_user_instance; @@ -3099,7 +3131,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("large write transactions which would be too large if batched") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); std::mutex mutex; bool done = false; @@ -3137,7 +3169,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("too large sync message error handling") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto pf = util::make_promise_future(); config.sync_config->error_handler = @@ -3176,7 +3208,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("freezing realm does not resume session") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto realm = Realm::get_shared_realm(config); wait_for_download(*realm); @@ -3203,7 +3235,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("pausing a session does not hold the DB open") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); DBRef dbref; std::shared_ptr sync_sess_ext_ref; { @@ -3244,7 +3276,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("validation") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); SECTION("invalid partition error handling") { config.sync_config->partition_value = "not a bson serialized string"; @@ -3256,7 +3288,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { error_did_occur.store(true); }; auto r = Realm::get_shared_realm(config); - auto session = app->current_user()->session_for_on_disk_path(r->config().path); + auto session = app->sync_manager()->get_existing_session(r->config().path); timed_wait_for([&] { return error_did_occur.load(); }); @@ -3290,7 +3322,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } SECTION("get_file_ident") { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard; auto r = Realm::get_shared_realm(config); wait_for_download(*r); @@ -3757,30 +3789,31 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } }; - auto redir_transport = std::make_shared(); util::LogCategory::realm.set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); auto logger = util::Logger::get_default_logger(); - App::Config app_config = {"app_id"}; - set_app_config_defaults(app_config, redir_transport); - - SyncClientConfig sc_config; - sc_config.base_file_path = util::make_temp_dir(); - sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; + auto redir_transport = std::make_shared(); + auto get_config_with_base_url = [&](std::optional base_url = std::nullopt) { + OfflineAppSession::Config config(redir_transport); + config.base_url = base_url; + return config; + }; SECTION("Test app config baseurl") { { + // First time through, base_url is empty; https://realm.mongodb.com is expected redir_transport->reset("https://realm.mongodb.com"); + auto config = get_config_with_base_url(); + OfflineAppSession oas(config); + auto app = oas.app(); - // First time through, base_url is empty; https://realm.mongodb.com is expected - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://realm.mongodb.com"); CHECK(app->get_ws_host_url() == "wss://realm.mongodb.com"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://realm.mongodb.com"); CHECK(app->get_host_url() == "https://realm.mongodb.com"); @@ -3788,17 +3821,18 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } { // Second time through, base_url is set to https://alternate.someurl.fake is expected - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://alternate.someurl.fake"); CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3807,19 +3841,20 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { { // Third time through, base_url is not set, expect https://realm.mongodb.com, since metadata // is no longer used - app_config.base_url = util::none; std::string expected_url = "https://realm.mongodb.com"; std::string expected_wsurl = "wss://realm.mongodb.com"; redir_transport->reset(expected_url); + auto config = get_config_with_base_url(); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == expected_url); CHECK(app->get_ws_host_url() == expected_wsurl); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == expected_url); CHECK(app->get_host_url() == expected_url); @@ -3827,17 +3862,18 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } { // Fourth time through, base_url is set to https://some-other.someurl.fake, with a redirect - app_config.base_url = "https://some-other.someurl.fake"; redir_transport->reset("https://some-other.someurl.fake", "http://redirect.someurl.fake"); + auto config = get_config_with_base_url("https://some-other.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://some-other.someurl.fake"); CHECK(app->get_ws_host_url() == "wss://some-other.someurl.fake"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); // Base URL is still set to the original value CHECK(app->get_base_url() == "https://some-other.someurl.fake"); @@ -3848,14 +3884,15 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } SECTION("Test update_baseurl") { - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3872,18 +3909,19 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK(app->get_host_url() == "https://realm.mongodb.com"); CHECK(app->get_ws_host_url() == "wss://realm.mongodb.com"); // Expected URL is still "https://realm.mongodb.com" - create_user_and_log_in(app); + oas.make_user(); } SECTION("Test update_baseurl with redirect") { - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3899,18 +3937,19 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK(app->get_host_url() == "https://redirect.otherurl.fake"); CHECK(app->get_ws_host_url() == "wss://redirect.otherurl.fake"); // Expected URL is still "https://redirect.otherurl.fake" after redirect - create_user_and_log_in(app); + oas.make_user(); } SECTION("Test update_baseurl returns error") { - app_config.base_url = "http://alternate.someurl.fake"; redir_transport->reset("http://alternate.someurl.fake"); + auto config = get_config_with_base_url("http://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "http://alternate.someurl.fake"); CHECK(app->get_host_url() == "http://alternate.someurl.fake"); @@ -3940,8 +3979,7 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { std::string redir_wsurl = util::format("ws%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port); auto socket_provider = std::make_shared(logger, "some user agent"); - socket_provider->endpoint_verify_func = [&use_ssl, &expected_host, - &expected_port](sync::WebSocketEndpoint& ep) { + socket_provider->endpoint_verify_func = [use_ssl, expected_host, expected_port](sync::WebSocketEndpoint& ep) { CHECK(ep.address == expected_host); CHECK(ep.port == expected_port); CHECK(ep.is_ssl == use_ssl); @@ -3954,20 +3992,21 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { return true; }; - sc_config.socket_provider = socket_provider; - sc_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - - app_config.base_url = init_url; + auto config = get_config_with_base_url(init_url); + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.socket_provider = socket_provider; + config.storage_path = util::make_temp_dir(); + config.delete_storage = false; // persist the current user // Log in to get a cached user { redir_transport->reset(init_url); - - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == init_url); @@ -3976,19 +4015,21 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK_THAT(*app->sync_manager()->sync_route(), ContainsSubstring(init_wsurl)); } + // the next instance can clean up the files + config.delete_storage = true; // Recreate the app using the cached user and start a sync session, which will is set to fail on connect SECTION("Sync Session fails on connect") { enum class TestState { start, session_started }; TestingStateMachine state(TestState::start); - redir_transport->reset(init_url, redir_url); - - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); + REQUIRE(app->current_user()); RealmConfig r_config; - r_config.path = sc_config.base_file_path + "/fakerealm.realm"; + r_config.path = app->config().base_file_path + "/fakerealm.realm"; r_config.sync_config = std::make_shared(app->current_user(), SyncConfig::FLXSyncEnabled{}); r_config.sync_config->error_handler = [&state, &logger](std::shared_ptr, SyncError error) mutable { @@ -3996,7 +4037,7 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { state.transition_with([&error, &logger](TestState cur_state) -> std::optional { if (cur_state == TestState::start) { // The session will start, but the connection is rejected on purpose - logger->debug("Expected error: %1", error.status); + logger->debug("Expected error: %1: %2", error.status.code_string(), error.status.reason()); CHECK(!error.status.is_ok()); CHECK(error.status.code() == ErrorCodes::SyncConnectFailed); return TestState::session_started; @@ -4018,16 +4059,17 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { SECTION("Location update fails prior to sync session connect") { enum class TestState { start, location_failed, waiting_for_session, session_started }; TestingStateMachine state(TestState::start); - redir_transport->reset(init_url, redir_url); redir_transport->location_returns_error = true; - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); + REQUIRE(app->current_user()); RealmConfig r_config; - r_config.path = sc_config.base_file_path + "/fakerealm.realm"; + r_config.path = app->config().base_file_path + "/fakerealm.realm"; r_config.sync_config = std::make_shared(app->current_user(), SyncConfig::FLXSyncEnabled{}); r_config.sync_config->error_handler = [&state, &logger](std::shared_ptr, SyncError error) mutable { @@ -4111,7 +4153,7 @@ TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][func SECTION("jwt happy path") { bool processed = false; - std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); + std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})}, [&](auto response, auto error) { @@ -4206,13 +4248,13 @@ TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][co SECTION("integration testing") { auto app = test_session.app(); - SyncTestFile config1(app, partition, schema); // uses the current user created above + SyncTestFile config1(app->current_user(), partition, schema); // uses the current user created above config1.automatic_change_notifications = false; auto r1 = realm::Realm::get_shared_realm(config1); Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source")); - create_user_and_log_in(app); - SyncTestFile config2(app, partition, schema); // uses the user created above + create_user_and_log_in(app); // changes the current user + SyncTestFile config2(app->current_user(), partition, schema); // uses the user created above config2.automatic_change_notifications = false; auto r2 = realm::Realm::get_shared_realm(config2); Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source")); @@ -4399,7 +4441,7 @@ TEST_CASE("app: full-text compatible with sync", "[sync][app][baas]") { auto app_session = create_app(server_app_config); const auto partition = random_string(100); TestAppSession test_session(app_session, nullptr); - SyncTestFile config(test_session.app(), partition, schema); + SyncTestFile config(test_session.app()->current_user(), partition, schema); SharedRealm realm; SECTION("sync open") { INFO("realm opened without async open"); @@ -4465,10 +4507,8 @@ TEST_CASE("app: custom error handling", "[sync][app][custom errors]") { }; SECTION("custom code and message is sent back") { - OfflineAppSession::Config config; - config.transport = std::make_shared(1001, "Boom!"); - OfflineAppSession oas(config); - auto error = failed_log_in(oas.app()); + OfflineAppSession offline_session({std::make_shared(1001, "Boom!")}); + auto error = failed_log_in(offline_session.app()); CHECK(error.is_custom_error()); CHECK(*error.additional_status_code == 1001); CHECK(error.reason() == "Boom!"); @@ -4550,23 +4590,22 @@ TEST_CASE("subscribable unit tests", "[sync][app]") { } TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { - OfflineAppSession::Config config{std::make_shared()}; - static_cast(config.transport.get())->set_profile(profile_0); + auto transport = std::make_shared(); + OfflineAppSession::Config config{transport}; + transport->set_profile(profile_0); SECTION("login_anonymous good") { - UnitTestTransport::access_token = good_access_token; - config.delete_storage = false; - config.metadata_mode = SyncManager::MetadataMode::NoEncryption; config.storage_path = util::make_temp_dir(); + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; { + config.delete_storage = false; OfflineAppSession oas(config); auto app = oas.app(); - auto user = log_in(app); REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - SyncUserProfile user_profile = user->user_profile(); + UserProfile user_profile = user->user_profile(); CHECK(user_profile.name() == profile_0_name); CHECK(user_profile.first_name() == profile_0_first_name); @@ -4578,7 +4617,6 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { CHECK(user_profile.min_age() == profile_0_min_age); CHECK(user_profile.max_age() == profile_0_max_age); } - App::clear_cached_apps(); // assert everything is stored properly between runs { config.delete_storage = true; // clean up after this session @@ -4588,7 +4626,7 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { auto user = app->all_users()[0]; REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id); - SyncUserProfile user_profile = user->user_profile(); + UserProfile user_profile = user->user_profile(); CHECK(user_profile.name() == profile_0_name); CHECK(user_profile.first_name() == profile_0_first_name); @@ -4619,14 +4657,13 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { config.transport = instance_of; OfflineAppSession oas(config); auto error = failed_log_in(oas.app()); - CHECK(error.reason() == std::string("malformed JWT")); + CHECK(error.reason() == std::string("Could not log in user: received malformed JWT")); CHECK(error.code_string() == "BadToken"); CHECK(error.is_json_error()); CHECK(error.code() == ErrorCodes::BadToken); } SECTION("login_anonynous multiple users") { - UnitTestTransport::access_token = good_access_token; OfflineAppSession oas(config); auto app = oas.app(); @@ -4680,9 +4717,8 @@ TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key } } - TEST_CASE("app: user_semantics", "[sync][app][user]") { - OfflineAppSession oas(instance_of); + OfflineAppSession oas; auto app = oas.app(); const auto login_user_email_pass = [=] { @@ -4701,28 +4737,28 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { SECTION("current user is populated") { const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->current_user()->user_id() == user1->user_id()); CHECK(event_processed == 1); } SECTION("current user is updated on login") { const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->current_user()->user_id() == user1->user_id()); const auto user2 = login_user_email_pass(); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() != user2->identity()); + CHECK(app->current_user()->user_id() == user2->user_id()); + CHECK(user1->user_id() != user2->user_id()); CHECK(event_processed == 2); } SECTION("current user is updated to last used user on logout") { const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->current_user()->user_id() == user1->user_id()); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); const auto user2 = login_user_email_pass(); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn); - CHECK(app->current_user()->identity() == user2->identity()); + CHECK(app->current_user()->user_id() == user2->user_id()); CHECK(user1 != user2); // should reuse existing session @@ -4736,8 +4772,8 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { app->log_out([](auto) {}); CHECK(user_events_processed == 1); - - CHECK(app->current_user()->identity() == user2->identity()); + REQUIRE(app->current_user()); + CHECK(app->current_user()->user_id() == user2->user_id()); CHECK(app->all_users().size() == 1); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); @@ -4747,14 +4783,14 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { SECTION("anon users are removed on logout") { const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->current_user()->user_id() == user1->user_id()); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); const auto user2 = login_user_anonymous(); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); CHECK(app->all_users().size() == 1); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() == user2->identity()); + CHECK(app->current_user()->user_id() == user2->user_id()); + CHECK(user1->user_id() == user2->user_id()); app->log_out([](auto) {}); CHECK(app->all_users().size() == 0); @@ -4778,7 +4814,7 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { }); CHECK(user1->state() == SyncUser::State::LoggedOut); - // Logging out already logged out users, does nothing + // Logging out already logged out users does nothing app->log_out(user1, [](Optional error) { REQUIRE_FALSE(error); }); @@ -4796,14 +4832,14 @@ TEST_CASE("app: user_semantics", "[sync][app][user]") { app->unsubscribe(token); const auto user1 = login_user_anonymous(); - CHECK(app->current_user()->identity() == user1->identity()); + CHECK(app->current_user()->user_id() == user1->user_id()); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); const auto user2 = login_user_anonymous(); CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn); CHECK(app->all_users().size() == 1); - CHECK(app->current_user()->identity() == user2->identity()); - CHECK(user1->identity() == user2->identity()); + CHECK(app->current_user()->user_id() == user2->user_id()); + CHECK(user1->user_id() == user2->user_id()); app->log_out([](auto) {}); CHECK(app->all_users().size() == 0); @@ -5064,7 +5100,7 @@ TEST_CASE("app: link_user", "[sync][app][user]") { app->link_user(sync_user, custom_credentials, [&](std::shared_ptr user, Optional error) { REQUIRE_FALSE(error); REQUIRE(user); - CHECK(user->identity() == sync_user->identity()); + CHECK(user->user_id() == sync_user->user_id()); processed = true; }); CHECK(processed); @@ -5329,18 +5365,16 @@ TEST_CASE("app: app released during async operation", "[app][user]") { } }; auto transport = std::make_shared(); - App::Config app_config; - set_app_config_defaults(app_config, transport); - SyncClientConfig sc_config; test_util::TestDirGuard base_path(util::make_temp_dir(), false); - sc_config.base_file_path = base_path; - sc_config.metadata_mode = SyncManager::MetadataMode::NoMetadata; + AppConfig app_config; + set_app_config_defaults(app_config, transport); + app_config.base_file_path = base_path; SECTION("login") { transport->endpoint_to_hook = GENERATE("/location", "/login", "/profile"); bool called = false; { - auto app = App::get_app(App::CacheMode::Disabled, app_config, sc_config); + auto app = App::get_app(App::CacheMode::Disabled, app_config); app->log_in_with_credentials(AppCredentials::anonymous(), [&](std::shared_ptr user, util::Optional error) mutable { REQUIRE_FALSE(error); @@ -5360,7 +5394,7 @@ TEST_CASE("app: app released during async operation", "[app][user]") { SECTION("directly via user") { bool completion_called = false; { - auto app = App::get_app(App::CacheMode::Disabled, app_config, sc_config); + auto app = App::get_app(App::CacheMode::Disabled, app_config); create_user_and_log_in(app); app->current_user()->refresh_custom_data([&](std::optional error) { REQUIRE_FALSE(error); @@ -5376,12 +5410,14 @@ TEST_CASE("app: app released during async operation", "[app][user]") { SECTION("via sync session") { { - auto app = App::get_app(App::CacheMode::Disabled, app_config, sc_config); + auto app = App::get_app(App::CacheMode::Disabled, app_config); create_user_and_log_in(app); auto user = app->current_user(); SyncTestFile config(user, bson::Bson("test")); // give the user an expired access token so that the first use will try to refresh it - user->update_access_token(encode_fake_jwt("token", 123, 456)); + user->update_data_for_testing([](auto& data) { + data.access_token = RealmJWT(encode_fake_jwt("token", 123, 456)); + }); REQUIRE_FALSE(transport->stored_completion); auto realm = Realm::get_shared_realm(config); REQUIRE(transport->has_stored()); @@ -5394,7 +5430,6 @@ TEST_CASE("app: app released during async operation", "[app][user]") { } TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { - UnitTestTransport::access_token = good_access_token; constexpr uint64_t timeout_ms = 60000; // this is the default OfflineAppSession oas({std::make_shared(timeout_ms)}); auto app = oas.app(); @@ -5420,7 +5455,7 @@ TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { return out; }; - const auto make_request = [&](std::shared_ptr user, auto&&... args) { + const auto make_request = [&](std::shared_ptr user, auto&&... args) { auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"}); CHECK(req.method == HttpMethod::get); CHECK(req.body == ""); @@ -5466,7 +5501,7 @@ TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") { SECTION("with empty map") { - auto profile = SyncUserProfile(bson::BsonDocument()); + auto profile = UserProfile(bson::BsonDocument()); CHECK(profile.name() == util::none); CHECK(profile.email() == util::none); CHECK(profile.picture_url() == util::none); @@ -5478,7 +5513,7 @@ TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") { CHECK(profile.max_age() == util::none); } SECTION("with full map") { - auto profile = SyncUserProfile(bson::BsonDocument({ + auto profile = UserProfile(bson::BsonDocument({ {"first_name", "Jan"}, {"last_name", "Jaanson"}, {"name", "Jan Jaanson"}, @@ -5504,12 +5539,9 @@ TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") { TEST_CASE("app: shared instances", "[sync][app]") { test_util::TestDirGuard test_dir(util::make_temp_dir(), false); - App::Config base_config; + AppConfig base_config; set_app_config_defaults(base_config, instance_of); - - SyncClientConfig sync_config; - sync_config.metadata_mode = SyncClientConfig::MetadataMode::NoMetadata; - sync_config.base_file_path = test_dir; + base_config.base_file_path = test_dir; auto config1 = base_config; config1.app_id = "app1"; @@ -5526,10 +5558,10 @@ TEST_CASE("app: shared instances", "[sync][app]") { config4.base_url = "http://localhost:9090"; // should all point to same underlying app - auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1, sync_config); - auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1, sync_config); + auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1); + auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1); auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url); - auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2, sync_config); + auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2); auto app1_5 = App::get_cached_app(config1.app_id); CHECK(app1_1 == app1_2); @@ -5538,9 +5570,9 @@ TEST_CASE("app: shared instances", "[sync][app]") { CHECK(app1_1 == app1_5); // config3 and config4 should point to different apps - auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3, sync_config); + auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3); auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url); - auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4, sync_config); + auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4); auto app2_4 = App::get_cached_app(config3.app_id); auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url"); diff --git a/test/object-store/sync/client_reset.cpp b/test/object-store/sync/client_reset.cpp index ee76f011bfb..0767f9af2ab 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -326,7 +326,7 @@ TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") { recovery_path = recovery_path_it->second; REQUIRE(util::File::exists(orig_path)); REQUIRE(!util::File::exists(recovery_path)); - bool did_reset_files = test_app_session.sync_manager()->immediately_run_file_actions(orig_path); + bool did_reset_files = test_app_session.app()->immediately_run_file_actions(orig_path); REQUIRE(did_reset_files); REQUIRE(!util::File::exists(orig_path)); REQUIRE(util::File::exists(recovery_path)); diff --git a/test/object-store/sync/file.cpp b/test/object-store/sync/file.cpp index 4d9cf0b1fd5..d7025b38c20 100644 --- a/test/object-store/sync/file.cpp +++ b/test/object-store/sync/file.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -132,8 +133,7 @@ TEST_CASE("sync_file: URL manipulation APIs", "[sync][file]") { } TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { - TestSyncManager tsm; - + realm::test_util::TestDirGuard test_dir(make_temp_dir(), false); const std::string identity = "abcdefghi"; const std::vector legacy_identities = {"legacy1", "legacy2"}; const auto& local_identity = legacy_identities[0]; @@ -141,10 +141,14 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { const std::string partition_str = random_string(10); const std::string partition = bson::Bson(partition_str).to_string(); const std::string expected_clean_app_id = "test_app_id%2A%24%23%40%21%251"; - const auto manager_base_path = fs::path{tsm.base_file_path()}.make_preferred() / "file-manager"; + const auto manager_base_path = fs::path{test_dir.c_str()}.make_preferred() / "file-manager"; util::try_make_dir(manager_base_path.string()); - const auto manager_path = manager_base_path / "mongodb-realm" / expected_clean_app_id; - auto manager = SyncFileManager(manager_base_path.string(), app_id); + const auto manager_path = manager_base_path / "mongodb-realm" / expected_clean_app_id / ""; + app::AppConfig config; + config.app_id = app_id; + config.base_file_path = manager_base_path; + auto manager = SyncFileManager(config); + REQUIRE(manager.app_path() == manager_path); SECTION("Realm path APIs") { auto relative_path = "s_" + partition_str; @@ -186,8 +190,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.fallback_hashed_path); REQUIRE(File::exists(expected_paths.fallback_hashed_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.fallback_hashed_path}); - REQUIRE(!File::exists(expected_paths.fallback_hashed_path)); } SECTION("legacy local identity path is detected and used") { @@ -203,8 +205,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.legacy_local_id_path); REQUIRE(File::exists(expected_paths.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.legacy_local_id_path}); - REQUIRE(!File::exists(expected_paths.legacy_local_id_path)); } SECTION("multiple legacy local identities are supported") { @@ -225,8 +225,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths_2.legacy_local_id_path); REQUIRE(File::exists(expected_paths_2.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths_2.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths_2.legacy_local_id_path}); - REQUIRE(!File::exists(expected_paths_2.legacy_local_id_path)); } SECTION("legacy sync paths are detected and used") { @@ -242,8 +240,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.legacy_sync_path); REQUIRE(File::exists(expected_paths.legacy_sync_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.legacy_sync_path}); - REQUIRE(!File::exists(expected_paths.legacy_sync_path)); } SECTION("paths have a fallback hashed location if the preferred path is too long") { @@ -253,8 +249,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual.length() < 500); REQUIRE(create_dummy_realm(actual)); REQUIRE(File::exists(actual)); - manager.remove_user_realms(identity, {actual}); - REQUIRE(!File::exists(actual)); } } diff --git a/test/object-store/sync/flx_migration.cpp b/test/object-store/sync/flx_migration.cpp index a4fee2cdb9c..ab0489b74a5 100644 --- a/test/object-store/sync/flx_migration.cpp +++ b/test/object-store/sync/flx_migration.cpp @@ -125,8 +125,8 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config1(session.app(), partition1, server_app_config.schema); - SyncTestFile config2(session.app(), partition2, server_app_config.schema); + SyncTestFile config1(session.app()->current_user(), partition1, server_app_config.schema); + SyncTestFile config2(session.app()->current_user(), partition2, server_app_config.schema); // Fill some objects auto objects1 = fill_test_data(config1, partition1); // 5 objects starting at 1 @@ -243,7 +243,7 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas } { - SyncTestFile pbs_config(session.app(), partition1, server_app_config.schema); + SyncTestFile pbs_config(session.app()->current_user(), partition1, server_app_config.schema); auto pbs_realm = Realm::get_shared_realm(pbs_config); REQUIRE(!wait_for_upload(*pbs_realm)); @@ -252,7 +252,7 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas check_data(pbs_realm, true, false); } { - SyncTestFile pbs_config(session.app(), partition2, server_app_config.schema); + SyncTestFile pbs_config(session.app()->current_user(), partition2, server_app_config.schema); auto pbs_realm = Realm::get_shared_realm(pbs_config); REQUIRE(!wait_for_upload(*pbs_realm)); @@ -273,7 +273,7 @@ TEST_CASE("Test client migration and rollback", "[sync][flx][flx migration][baas }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -328,7 +328,7 @@ TEST_CASE("Test client migration and rollback with recovery", "[sync][flx][flx m }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::Recover; config.schema_version = 0; @@ -484,7 +484,7 @@ TEST_CASE("An interrupted migration or rollback can recover on the next session" }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -621,7 +621,7 @@ TEST_CASE("Update to native FLX after migration", "[sync][flx][flx migration][ba }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -742,7 +742,7 @@ TEST_CASE("New table is synced after migration", "[sync][flx][flx migration][baa const Schema two_obj_schema{obj1_schema, obj2_schema}; auto server_app_config = minimal_app_config("server_migrate_rollback", two_obj_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -847,7 +847,7 @@ TEST_CASE("Async open + client reset", "[sync][flx][flx migration][baas]") { server_app_config.dev_mode_enabled = true; std::optional config; // destruct this after the sessions are torn down TestAppSession session(create_app(server_app_config)); - config.emplace(session.app(), partition, server_app_config.schema); + config.emplace(session.app()->current_user(), partition, server_app_config.schema); config->sync_config->client_resync_mode = ClientResyncMode::Recover; config->sync_config->notify_before_client_reset = [&](SharedRealm before) { logger_ptr->debug("notify_before_client_reset"); diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index 3ac457c8e3e..cc286298927 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -2613,7 +2613,7 @@ TEST_CASE("flx: subscriptions persist after closing/reopening", "[sync][flx][baa TEST_CASE("flx: no subscription store created for PBS app", "[sync][flx][baas]") { auto server_app_config = minimal_app_config("flx_connect_as_pbs", g_minimal_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), bson::Bson{}, g_minimal_schema); + SyncTestFile config(session.app()->current_user(), bson::Bson{}, g_minimal_schema); auto realm = Realm::get_shared_realm(config); CHECK(!wait_for_download(*realm)); @@ -2627,7 +2627,7 @@ TEST_CASE("flx: no subscription store created for PBS app", "[sync][flx][baas]") TEST_CASE("flx: connect to FLX as PBS returns an error", "[sync][flx][baas]") { FLXSyncTestHarness harness("connect_to_flx_as_pbs"); - SyncTestFile config(harness.app(), bson::Bson{}, harness.schema()); + SyncTestFile config(harness.app()->current_user(), bson::Bson{}, harness.schema()); std::mutex sync_error_mutex; util::Optional sync_error; config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) mutable { @@ -2694,14 +2694,16 @@ TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][ auto transport = std::make_shared(); FLXSyncTestHarness harness("flx_wait_access_token2", FLXSyncTestHarness::default_server_schema(), transport); auto app = harness.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); using namespace std::chrono_literals; auto expires = std::chrono::system_clock::to_time_t(now - 30s); - user->update_access_token(encode_fake_jwt("fake_access_token", expires)); + user->update_data_for_testing([&](UserData& data) { + data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", expires)); + }); REQUIRE(user->access_token_refresh_required()); bool seen_waiting_for_access_token = false; @@ -2710,7 +2712,7 @@ TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][ transport->request_hook = [&](const Request&) { auto user = app->current_user(); REQUIRE(user); - for (auto& session : user->all_sessions()) { + for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) { if (session->state() == SyncSession::State::WaitingForAccessToken) { REQUIRE(!seen_waiting_for_access_token); seen_waiting_for_access_token = true; @@ -3438,7 +3440,7 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { }}, }; - SyncTestFile config(harness->app(), Bson{}, schema); + SyncTestFile config(harness->app()->current_user(), Bson{}, schema); REQUIRE_EXCEPTION( Realm::get_shared_realm(config), SchemaValidationFailed, Catch::Matchers::ContainsSubstring("Asymmetric table 'Asymmetric2' not allowed in partition based sync")); @@ -3529,8 +3531,9 @@ TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") { Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}})); Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}})); realm->commit_transaction(); - - auto docs = harness.session().get_documents(*realm->config().sync_config->user, "Asymmetric", 2); + User* user = dynamic_cast(realm->config().sync_config->user.get()); + REALM_ASSERT(user); + auto docs = harness.session().get_documents(*user, "Asymmetric", 2); check_document(docs, foo_obj_id, {{"location", "foo"}}); check_document(docs, bar_obj_id, {{"location", "bar"}}); }, diff --git a/test/object-store/sync/metadata.cpp b/test/object-store/sync/metadata.cpp index 65b76469992..01db12fc6bd 100644 --- a/test/object-store/sync/metadata.cpp +++ b/test/object-store/sync/metadata.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////// // -// Copyright 2016 Realm Inc. +// 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. @@ -16,422 +16,670 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include -#include - -#include -#include -#include +#include #include -#include -#include - +#include +#include +#include #include #include +#include +#include + #include using namespace realm; -using namespace realm::util; -using File = realm::util::File; -using SyncAction = SyncFileActionMetadata::Action; +using namespace realm::app; +using realm::util::File; + +namespace { +const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir"; +const std::string metadata_path = base_path + "/mongodb-realm/app%20id/server-utility/metadata/sync_metadata.realm"; +constexpr const char* user_id = "user_id"; +constexpr const char* device_id = "device_id"; +const auto access_token = encode_fake_jwt("access_token", 123, 456); +const auto refresh_token = encode_fake_jwt("refresh_token", 123, 456); + +std::shared_ptr get_metadata_realm() +{ + RealmConfig realm_config; + realm_config.automatic_change_notifications = false; + realm_config.path = metadata_path; + return Realm::get_shared_realm(std::move(realm_config)); +} +} // anonymous namespace + +namespace realm::app { +static std::ostream& operator<<(std::ostream& os, AppConfig::MetadataMode mode) +{ + switch (mode) { + case AppConfig::MetadataMode::InMemory: + os << "InMemory"; + break; + case AppConfig::MetadataMode::NoEncryption: + os << "NoEncryption"; + break; + case AppConfig::MetadataMode::Encryption: + os << "Encryption"; + break; + default: + os << "unknown"; + break; + } + return os; +} +} // namespace realm::app -static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir"; -static const std::string metadata_path = base_path + "/metadata.realm"; +using Strings = std::vector; -TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { +TEST_CASE("app metadata: common", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - - SECTION("can be properly constructed") { - const auto identity = "testcase1a"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token().empty()); - } - - SECTION("properly reflects updating state") { - const auto identity = "testcase1b"; - const std::string sample_token = "this_is_a_user_token"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - user_metadata->set_access_token(sample_token); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); - } - - SECTION("can be properly re-retrieved from the same manager") { - const auto identity = "testcase1c"; - const std::string sample_token = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - // Get a second instance of the user metadata for the same identity. - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - } - - SECTION("properly reflects changes across different instances") { - const auto identity = "testcase1d"; - const std::string sample_token_1 = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity); - auto second = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token_1); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token_1); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token_1); - // Set the state again. - const std::string sample_token_2 = "this_is_another_user_token"; - second->set_access_token(sample_token_2); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token_2); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token_2); - } - - SECTION("can be removed") { - const auto identity = "testcase1e"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->is_valid()); - user_metadata->remove(); - REQUIRE(!user_metadata->is_valid()); - } - - SECTION("respects make_if_absent flag set to false in constructor") { - const std::string sample_token = "this_is_a_user_token"; - - SECTION("with no prior metadata for the identifier") { - const auto identity = "testcase1g1"; - auto user_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(!user_metadata); - } - SECTION("with valid prior metadata for the identifier") { - const auto identity = "testcase1g2"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->is_valid()); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - } - SECTION("with invalid prior metadata for the identifier") { - const auto identity = "testcase1g3"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - first->set_state(SyncUser::State::Removed); - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(!second); - } + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = GENERATE(AppConfig::MetadataMode::InMemory, AppConfig::MetadataMode::NoEncryption); + config.base_file_path = base_path; + SyncFileManager file_manager(config); + auto store = create_metadata_store(config, file_manager); + + INFO(config.metadata_mode); + + SECTION("create_user() creates new logged-in users") { + REQUIRE_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + REQUIRE(store->has_logged_in_user(user_id)); + auto data = store->get_user(user_id); + REQUIRE(data); + REQUIRE(data->access_token.token == access_token); + REQUIRE(data->refresh_token.token == refresh_token); + REQUIRE(data->device_id == device_id); } -} -TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - const std::string provider_type = "https://realm.example.org"; + SECTION("passing malformed tokens create_user() results in a logged out user") { + store->create_user(user_id, refresh_token, "not a token", device_id); + auto data = store->get_user(user_id); + REQUIRE(data); + REQUIRE(data->access_token.token == ""); + REQUIRE(data->refresh_token.token == ""); + REQUIRE(data->device_id == device_id); + } - SECTION("properly list all marked and unmarked users") { - const auto identity1 = "testcase2a1"; - const auto identity2 = "testcase2a3"; - auto first = manager.get_or_make_user_metadata(identity1); - auto second = manager.get_or_make_user_metadata(identity1); - auto third = manager.get_or_make_user_metadata(identity2); - auto unmarked_users = manager.all_unmarked_users(); - REQUIRE(unmarked_users.size() == 2); - REQUIRE(results_contains_user(unmarked_users, identity1)); - REQUIRE(results_contains_user(unmarked_users, identity2)); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Now, mark a few users for removal. - first->set_state(SyncUser::State::Removed); - unmarked_users = manager.all_unmarked_users(); - REQUIRE(unmarked_users.size() == 1); - REQUIRE(results_contains_user(unmarked_users, identity2)); - marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 1); - REQUIRE(results_contains_user(marked_users, identity1)); + SECTION("create_user() marks the new user as the current user if it was created") { + CHECK(store->get_current_user() == ""); + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->get_current_user() == user_id); + store->create_user("user 2", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); } -} -TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - - const std::string local_uuid_1 = "asdfg"; - const std::string local_uuid_2 = "qwerty"; - const std::string url_1 = "realm://realm.example.com/1"; - const std::string url_2 = "realm://realm.example.com/2"; - - SECTION("can be properly constructed") { - const auto original_name = util::make_temp_dir() + "foobar/test1"; - manager.make_file_action_metadata(original_name, SyncAction::BackUpThenDeleteRealm); - auto metadata = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata.original_name() == original_name); - REQUIRE(metadata.new_name() == none); - REQUIRE(metadata.action() == SyncAction::BackUpThenDeleteRealm); - } - - SECTION("properly reflects updating state, across multiple instances") { - const auto original_name = util::make_temp_dir() + "foobar/test2a"; - const std::string new_name_1 = util::make_temp_dir() + "foobar/test2b"; - const std::string new_name_2 = util::make_temp_dir() + "foobar/test2c"; - - manager.make_file_action_metadata(original_name, SyncAction::BackUpThenDeleteRealm, new_name_1); - auto metadata_1 = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata_1.original_name() == original_name); - REQUIRE(metadata_1.new_name() == new_name_1); - REQUIRE(metadata_1.action() == SyncAction::BackUpThenDeleteRealm); - - manager.make_file_action_metadata(original_name, SyncAction::DeleteRealm, new_name_2); - auto metadata_2 = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata_1.original_name() == original_name); - REQUIRE(metadata_1.new_name() == new_name_2); - REQUIRE(metadata_1.action() == SyncAction::DeleteRealm); - REQUIRE(metadata_2.original_name() == original_name); - REQUIRE(metadata_2.new_name() == new_name_2); - REQUIRE(metadata_2.action() == SyncAction::DeleteRealm); + SECTION("create_user() only updates the given fields and leaves the rest unchanged") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto data = store->get_user(user_id); + REQUIRE(data); + data->profile = bson::BsonDocument{{"name", "user's name"}, {"email", "user's email"}}; + data->identities = {{"identity", "provider"}}; + store->update_user(user_id, *data); + + const auto access_token_2 = encode_fake_jwt("access_token_2", 123, 456); + const auto refresh_token_2 = encode_fake_jwt("refresh_token_2", 123, 456); + store->create_user(user_id, refresh_token_2, access_token_2, "device id 2"); + + auto data2 = store->get_user(user_id); + REQUIRE(data2); + CHECK(data2->access_token.token == access_token_2); + CHECK(data2->refresh_token.token == refresh_token_2); + CHECK(data2->legacy_identities.empty()); + CHECK(data2->device_id == "device id 2"); + CHECK(data2->identities == data->identities); + CHECK(data2->profile.data() == data->profile.data()); + } + + SECTION("has_logged_in_user() is only true if user is present and valid") { + CHECK_FALSE(store->has_logged_in_user("")); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, "malformed token", device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, "", device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, "malformed token", access_token, device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, "", access_token, device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::LoggedOut); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::Removed); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->has_logged_in_user(user_id)); + CHECK_FALSE(store->has_logged_in_user("")); + CHECK_FALSE(store->has_logged_in_user("different user")); + } + + SECTION("get_all_users() returns all non-removed users") { + store->create_user("user 1", refresh_token, access_token, device_id); + store->create_user("user 2", refresh_token, access_token, device_id); + store->create_user("user 3", refresh_token, access_token, device_id); + store->create_user("user 4", refresh_token, access_token, device_id); + + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3", "user 4"}); + + store->log_out("user 2", SyncUser::State::LoggedOut); + store->delete_user(file_manager, "user 4"); + + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->create_user("user 1", "", access_token, device_id); + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->create_user("user 3", refresh_token, "", device_id); + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(!store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->delete_user(file_manager, "user 1"); + store->delete_user(file_manager, "user 2"); + store->delete_user(file_manager, "user 3"); + store->delete_user(file_manager, "user 4"); + CHECK(store->get_all_users().empty()); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(!store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + } + + SECTION("set_current_user() sets to the requested user") { + CHECK(store->get_current_user() == ""); + store->create_user("user 1", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 1"); + store->create_user("user 2", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); + + store->set_current_user(""); + CHECK(store->get_current_user() == "user 1"); + store->set_current_user("user 2"); + CHECK(store->get_current_user() == "user 2"); + store->set_current_user("user 1"); + CHECK(store->get_current_user() == "user 1"); + } + + SECTION("current user falls back to the first valid one if current is invalid") { + store->create_user("user 1", refresh_token, access_token, device_id); + store->create_user("user 2", refresh_token, access_token, device_id); + store->create_user("user 3", refresh_token, access_token, device_id); + + auto data = store->get_user("user 3"); + data->access_token.token.clear(); + data->refresh_token.token.clear(); + store->update_user("user 3", *data); + CHECK(store->get_current_user() == "user 1"); + store->update_user("user 1", *data); + CHECK(store->get_current_user() == "user 2"); + + store->set_current_user("not a user"); + CHECK(store->get_current_user() == "user 2"); + store->set_current_user(""); + CHECK(store->get_current_user() == "user 2"); + } + + SECTION("log_out() updates the user state without deleting anything") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto path = File::resolve("file 1", base_path); + File(path, File::mode_Write); + CHECK(File::exists(path)); + store->add_realm_path(user_id, path); + store->add_realm_path(user_id, "invalid path"); + store->log_out(user_id, SyncUser::State::Removed); + CHECK(File::exists(path)); + } + + SECTION("delete_user() deletes the files recorded with add_realm_file_path()") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto path = File::resolve("file 1", base_path); + File(path, File::mode_Write); + CHECK(File::exists(path)); + store->add_realm_path(user_id, path); + store->add_realm_path(user_id, "invalid path"); + store->delete_user(file_manager, user_id); + CHECK_FALSE(File::exists(path)); + } + + SECTION("update_user() does not set legacy identities") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto data = store->get_user(user_id); + data->legacy_identities.push_back("legacy uuid"); + store->update_user(user_id, *data); + data = store->get_user(user_id); + REQUIRE(data->legacy_identities.empty()); + } + + SECTION("immediately run nonexistent action") { + CHECK_FALSE(store->immediately_run_file_actions(file_manager, "invalid")); + } + + SECTION("immediately run DeleteRealm action") { + auto path = util::make_temp_file("delete-realm-action"); + store->create_file_action(SyncFileAction::DeleteRealm, path, {}); + CHECK(File::exists(path)); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } + + SECTION("immediately run BackUpThenDeleteRealm action") { + auto path = util::make_temp_file("delete-realm-action"); + auto backup_path = util::make_temp_file("backup-path"); + File::remove(backup_path); + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path); + CHECK(File::exists(path)); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK(File::exists(backup_path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } + + SECTION("file actions replace existing ones for the same path") { + auto path = util::make_temp_file("delete-realm-action"); + auto backup_path = util::make_temp_file("backup-path"); + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path); + store->create_file_action(SyncFileAction::DeleteRealm, path, {}); + CHECK(File::exists(path)); + // Would return false if it tried to perform a backup + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + } + + SECTION("failed backup action is preserved") { + auto path = util::make_temp_file("delete-realm-action"); + auto backup_path = util::make_temp_file("backup-path"); + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path); + CHECK(File::exists(path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + File::remove(backup_path); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK(File::exists(backup_path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } + +#if REALM_PLATFORM_APPLE + SECTION("failed delete after backup succeeds turns into a delete action") { + auto path = util::make_temp_file("delete-realm-action"); + auto backup_path = util::make_temp_file("backup-path"); + File::remove(backup_path); + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path); + CHECK(File::exists(path)); + + REQUIRE(chflags(path.c_str(), UF_IMMUTABLE) == 0); + // Returns false because it did something, but did not complete + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + CHECK(File::exists(path)); + CHECK(File::exists(backup_path)); + + // Should try again to remove the original file, but not perform another backup + REQUIRE(chflags(path.c_str(), 0) == 0); + REQUIRE(chflags(backup_path.c_str(), 0) == 0); + File::remove(backup_path); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK_FALSE(File::exists(backup_path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } +#endif + + SECTION("file action on deleted file is considered successful") { + auto path = util::make_temp_file("delete-realm-action"); + File::remove(path); + + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, path); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + + store->create_file_action(SyncFileAction::DeleteRealm, path, {}); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); } } -TEST_CASE("sync_metadata: file action metadata APIs", "[sync][metadata]") { +TEST_CASE("app metadata: in memory", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::InMemory; + config.base_file_path = base_path; + SyncFileManager file_manager(config); - SyncMetadataManager manager(metadata_path, false); - SECTION("properly list all pending actions, reflecting their deletion") { - const auto filename1 = util::make_temp_dir() + "foobar/file1"; - const auto filename2 = util::make_temp_dir() + "foobar/file2"; - const auto filename3 = util::make_temp_dir() + "foobar/file3"; - manager.make_file_action_metadata(filename1, SyncAction::BackUpThenDeleteRealm); - manager.make_file_action_metadata(filename2, SyncAction::BackUpThenDeleteRealm); - manager.make_file_action_metadata(filename3, SyncAction::BackUpThenDeleteRealm); - auto actions = manager.all_pending_actions(); - REQUIRE(actions.size() == 3); - REQUIRE(results_contains_original_name(actions, filename1)); - REQUIRE(results_contains_original_name(actions, filename2)); - REQUIRE(results_contains_original_name(actions, filename3)); - manager.get_file_action_metadata(filename1)->remove(); - manager.get_file_action_metadata(filename2)->remove(); - manager.get_file_action_metadata(filename3)->remove(); - REQUIRE(actions.size() == 0); + SECTION("does not persist users between instances") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + } } } -TEST_CASE("sync_metadata: results", "[sync][metadata]") { +TEST_CASE("app metadata: persisted", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - const auto identity1 = "testcase3a1"; - const auto identity2 = "testcase3a3"; - - SECTION("properly update as underlying items are added") { - auto results = manager.all_unmarked_users(); - REQUIRE(results.size() == 0); - // Add users, one at a time. - auto first = manager.get_or_make_user_metadata(identity1); - REQUIRE(results.size() == 1); - REQUIRE(results_contains_user(results, identity1)); - auto second = manager.get_or_make_user_metadata(identity2); - REQUIRE(results.size() == 2); - REQUIRE(results_contains_user(results, identity2)); - } - - SECTION("properly update as underlying items are removed") { - auto results = manager.all_unmarked_users(); - auto first = manager.get_or_make_user_metadata(identity1); - auto second = manager.get_or_make_user_metadata(identity2); - REQUIRE(results.size() == 2); - REQUIRE(results_contains_user(results, identity1)); - REQUIRE(results_contains_user(results, identity2)); - // Remove users, one at a time. - first->remove(); - REQUIRE(results.size() == 1); - REQUIRE(!results_contains_user(results, identity1)); - second->remove(); - REQUIRE(results.size() == 0); + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.base_file_path = base_path; + SyncFileManager file_manager(config); + + SECTION("persists users between instances") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + store->log_out(user_id, SyncUser::State::LoggedOut); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + CHECK(store->get_all_users() == Strings{user_id}); + } } -} -TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync][metadata]") { - test_util::TestDirGuard temp_dir(base_path); - - SECTION("works for the basic case") { - const auto identity = "testcase4a"; - const std::string provider_type = "any-type"; - const std::string sample_token = "this_is_a_user_token"; - SyncMetadataManager first_manager(metadata_path, false); - auto first = first_manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - first->set_state(SyncUser::State::LoggedOut); - - SyncMetadataManager second_manager(metadata_path, false); - auto second = second_manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - REQUIRE(second->state() == SyncUser::State::LoggedOut); + SECTION("can read legacy identities if present") { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + + auto data = store->get_user(user_id); + CHECK(data->legacy_identities.empty()); + + { + // Add some legacy uuids by modifying the underlying realm directly + auto realm = get_metadata_realm(); + auto table = realm->read_group().get_table("class_UserMetadata"); + REQUIRE(table); + REQUIRE(table->size() == 1); + auto list = table->begin()->get_list("legacy_uuids"); + realm->begin_transaction(); + list.add("uuid 1"); + list.add("uuid 2"); + realm->commit_transaction(); + } + + data = store->get_user(user_id); + CHECK(data->legacy_identities == std::vector{"uuid 1", "uuid 2"}); } -} -TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - const auto identity0 = "identity0"; - SECTION("prohibits opening the metadata Realm with different keys") { - SECTION("different keys") { - { - // Open metadata realm, make metadata - std::vector key0 = make_test_encryption_key(10); - SyncMetadataManager manager0(metadata_path, true, key0); - - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); - REQUIRE(bool(user_metadata0)); - CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->access_token().empty()); - CHECK(user_metadata0->is_valid()); - } - // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope - // Open new metadata realm at path with different key - std::vector key1 = make_test_encryption_key(11); - SyncMetadataManager manager1(metadata_path, true, key1); - - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); - // Expect previous metadata to have been deleted - CHECK_FALSE(bool(user_metadata1)); - - // But new metadata can still be created - const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); - CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->access_token().empty()); - CHECK(user_metadata2->is_valid()); + SECTION("runs file actions on creation") { + auto path = util::make_temp_file("file_to_delete"); + auto nonexistent = util::make_temp_file("nonexistent"); + File::remove(nonexistent); + + { + auto store = create_metadata_store(config, file_manager); + store->create_file_action(SyncFileAction::DeleteRealm, path, ""); + store->create_file_action(SyncFileAction::DeleteRealm, nonexistent, ""); } - SECTION("different encryption settings") { - { - // Encrypt metadata realm at path, make metadata - SyncMetadataManager manager0(metadata_path, true, make_test_encryption_key(10)); - - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); - REQUIRE(bool(user_metadata0)); - CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->access_token().empty()); - CHECK(user_metadata0->is_valid()); - } - // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope - // Open new metadata realm at path with different encryption configuration - SyncMetadataManager manager1(metadata_path, false); - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); - // Expect previous metadata to have been deleted - CHECK_FALSE(bool(user_metadata1)); - - // But new metadata can still be created - const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); - CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->access_token().empty()); - CHECK(user_metadata2->is_valid()); + + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + REQUIRE_FALSE(File::exists(nonexistent)); + + // Check the underlying realm to verify both file actions are gone + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_FileActionMetadata")->is_empty()); + } + + SECTION("deletes data for removed users on creation") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::Removed); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->get_all_users().empty()); } + // Check the underlying realm as removed users aren't exposed in the API + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty()); + } + + SECTION("deletes realm files for removed users on creation") { + auto path = util::make_temp_file("file_to_delete"); + auto nonexistent = util::make_temp_file("nonexistent"); + REQUIRE(File::exists(path)); + File::remove(nonexistent); + + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, nonexistent); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); + } + + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + REQUIRE_FALSE(File::exists(nonexistent)); } - SECTION("works when enabled") { - std::vector key = make_test_encryption_key(10); - const auto identity = "testcase5a"; - SyncMetadataManager manager(metadata_path, true, key); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(bool(user_metadata)); - CHECK(user_metadata->identity() == identity); - CHECK(user_metadata->access_token().empty()); - CHECK(user_metadata->is_valid()); - // Reopen the metadata file with the same key. - SyncMetadataManager manager_2(metadata_path, true, key); - auto user_metadata_2 = manager_2.get_or_make_user_metadata(identity, false); - REQUIRE(bool(user_metadata_2)); - CHECK(user_metadata_2->identity() == identity); - CHECK(user_metadata_2->is_valid()); - } - - SECTION("enabled without custom encryption key") { #if REALM_PLATFORM_APPLE - static bool can_access_keychain = [] { - bool can_access = keychain::create_new_metadata_realm_key() != none; - if (!can_access) { - std::cout << "Skipping keychain tests as the keychain is not accessible\n"; - } - return can_access; - }(); - if (!can_access_keychain) { - return; + SECTION("continues tracking files to delete if deletion fails") { + auto path = util::make_temp_file("file_to_delete"); + REQUIRE(File::exists(path)); + + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); } - auto delete_key = util::make_scope_exit([=]() noexcept { - keychain::delete_metadata_realm_encryption_key(); - }); - SECTION("automatically generates an encryption key for new files") { - { - SyncMetadataManager manager(metadata_path, true, none); - manager.set_current_user_identity(identity0); - } + REQUIRE(chflags(path.c_str(), UF_IMMUTABLE) == 0); + create_metadata_store(config, file_manager); + REQUIRE(File::exists(path)); + REQUIRE(chflags(path.c_str(), 0) == 0); + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + } +#endif - // Should be able to reopen and read data - { - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == identity0); - } + SECTION("stops tracking files if it no longer exists") { + auto path = util::make_temp_file("nonexistent"); + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); + } + + File::remove(path); + create_metadata_store(config, file_manager); + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty()); + } + + SECTION("deletes legacy untracked files") { + } +} - // Verify that the file is actually encrypted - REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, - Catch::Matchers::ContainsSubstring("invalid mnemonic")); +TEST_CASE("app metadata: encryption", "[sync][metadata]") { + test_util::TestDirGuard test_dir(base_path); + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::Encryption; + config.custom_encryption_key = make_test_encryption_key(10); + config.base_file_path = base_path; + SyncFileManager file_manager(config); + + // Verify that the Realm is actually encrypted with the expected key + auto open_realm_with_key = [](auto& key) { + RealmConfig realm_config; + realm_config.automatic_change_notifications = false; + realm_config.path = metadata_path; + // sanity check that using the wrong key throws, as otherwise we'd pass + // if we were checking the wrong path + realm_config.encryption_key = make_test_encryption_key(0); + CHECK_THROWS(Realm::get_shared_realm(realm_config)); + + if (key) { + realm_config.encryption_key = *key; + } + else { + realm_config.encryption_key.clear(); } + CHECK_NOTHROW(Realm::get_shared_realm(realm_config)); + }; - SECTION("leaves existing unencrypted files unencrypted") { - { - SyncMetadataManager manager(metadata_path, false, none); - manager.set_current_user_identity(identity0); - } - { - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == identity0); - } - REQUIRE_NOTHROW(Group(metadata_path)); + SECTION("can open and reopen with an explicit key") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(config.custom_encryption_key); + } + + SECTION("reopening with a different key deletes the existing data") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); } + open_realm_with_key(config.custom_encryption_key); - SECTION("recreates the file if the old encryption key was lost") { - { - SyncMetadataManager manager(metadata_path, true, none); - manager.set_current_user_identity(identity0); - } + // Change to new encryption key + { + config.custom_encryption_key = make_test_encryption_key(11); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + // Change to unencrypted + { + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.custom_encryption_key.reset(); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + + // Change back to encrypted + { + config.metadata_mode = AppConfig::MetadataMode::Encryption; + config.custom_encryption_key = make_test_encryption_key(12); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + } + +#if REALM_PLATFORM_APPLE + static bool can_access_keychain = [] { + bool can_access = keychain::create_new_metadata_realm_key() != none; + if (can_access) { keychain::delete_metadata_realm_encryption_key(); + } + else { + std::cout << "Skipping keychain tests as the keychain is not accessible\n"; + } + return can_access; + }(); + if (!can_access_keychain) { + return; + } + auto delete_key = util::make_scope_exit([=]() noexcept { + keychain::delete_metadata_realm_encryption_key(); + }); - { - // File should now be missing the data - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == none); - } - // New file should be encrypted - REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, - Catch::Matchers::ContainsSubstring("invalid mnemonic")); + SECTION("encryption key is automatically generated and stored for new files") { + config.custom_encryption_key.reset(); + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); } -#else - REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, true, none), InvalidArgument, + auto key = keychain::get_existing_metadata_realm_key(); + REQUIRE(key); + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(key); + } + + SECTION("existing unencrypted files are left unencrypted") { + config.custom_encryption_key.reset(); + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + + config.metadata_mode = AppConfig::MetadataMode::Encryption; + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(config.custom_encryption_key); + } +#else // REALM_PLATFORM_APPLE + SECTION("requires an explicit encryption key") { + config.custom_encryption_key.reset(); + REQUIRE_EXCEPTION(create_metadata_store(config, file_manager), InvalidArgument, "Metadata Realm encryption was specified, but no encryption key was provided."); -#endif } +#endif // REALM_PLATFORM_APPLE } #ifndef SWIFT_PACKAGE // The SPM build currently doesn't copy resource files TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); + util::make_dir_recursive(File::parent_dir(metadata_path)); + const std::string provider_type = "https://realm.example.org"; const auto identity = "metadata migration test"; - const std::string sample_token = "metadata migration token"; + const std::string sample_token = encode_fake_jwt("metadata migration token", 456, 123); const auto access_token_1 = encode_fake_jwt("access token 1", 456, 123); const auto access_token_2 = encode_fake_jwt("access token 2", 456, 124); const auto refresh_token_1 = encode_fake_jwt("refresh token 1", 456, 123); const auto refresh_token_2 = encode_fake_jwt("refresh token 2", 456, 124); + AppConfig config; + config.app_id = "app id"; + config.base_file_path = base_path; + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + SyncFileManager file_manager(config); + + // change to true to create a test file for the current schema version // this will only work on unix-like systems if ((false)) { @@ -457,7 +705,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { user->set_identities({{"identity 1", "a"}, {"shared identity", "shared"}}); user->add_realm_file_path("file 1"); user->add_realm_file_path("file 2"); - + user = manager.get_or_make_user_metadata(name, "b"); user->set_state_and_tokens(state2, token_2, refresh_token_2); user->set_identities({{"identity 2", "b"}, {"shared identity", "shared"}}); @@ -496,16 +744,13 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { } #else { // Create a metadata Realm with a test user - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - user_metadata->set_access_token(sample_token); + auto store = create_metadata_store(config, file_manager); + store->create_user(identity, sample_token, sample_token, "device id"); } #endif // Open the metadata Realm directly and grab the schema version from it - Realm::Config config; - config.path = metadata_path; - auto realm = Realm::get_shared_realm(config); + auto realm = get_metadata_realm(); realm->read_group(); auto schema_version = realm->schema_version(); @@ -528,53 +773,45 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 4") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v4.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); + auto store = create_metadata_store(config, file_manager); + auto user_metadata = store->get_user(identity); + REQUIRE(user_metadata->access_token.token == sample_token); } SECTION("open schema version 5") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v5.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); + auto store = create_metadata_store(config, file_manager); + auto user_metadata = store->get_user(identity); + REQUIRE(user_metadata->access_token.token == sample_token); } SECTION("open schema version 6") { - using State = SyncUser::State; File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); + auto store = create_metadata_store(config, file_manager); - SyncUserIdentity id_1{"identity 1", "a"}; - SyncUserIdentity id_2{"identity 2", "b"}; - SyncUserIdentity id_shared{"shared identity", "shared"}; - const std::vector all_ids = {id_1, id_shared, id_2}; + UserIdentity id_1{"identity 1", "a"}; + UserIdentity id_2{"identity 2", "b"}; + UserIdentity id_shared{"shared identity", "shared"}; + const std::vector all_ids = {id_1, id_shared, id_2}; const std::vector realm_files = {"file 1", "file 2", "file 3"}; - auto check_user = [&](const char* user_id, State state, const std::string& access_token, - const std::string& refresh_token, const std::vector& uuids) { - auto user = manager.get_or_make_user_metadata(user_id, false); + auto check_user = [&](const char* user_id, const std::string& access_token, const std::string& refresh_token, + const std::vector& uuids) { + auto user = store->get_user(user_id); CAPTURE(user_id); - REQUIRE(user); - CHECK(user->state() == state); - CHECK(user->access_token() == access_token); - CHECK(user->refresh_token() == refresh_token); - CHECK(user->legacy_identities() == uuids); - CHECK(user->identities() == all_ids); - CHECK(user->realm_file_paths() == realm_files); + CHECK(user->access_token.token == access_token); + CHECK(user->refresh_token.token == refresh_token); + CHECK(user->legacy_identities == uuids); + CHECK(user->identities == all_ids); }; - REQUIRE_FALSE(manager.get_or_make_user_metadata("removed user", false)); - check_user("first logged in, second logged out", State::LoggedIn, access_token_1, refresh_token_1, - {"1", "2"}); - check_user("first logged in, second removed", State::LoggedIn, access_token_1, refresh_token_1, {"3", "4"}); - check_user("second logged in, first logged out", State::LoggedIn, access_token_2, refresh_token_2, - {"5", "6"}); - check_user("second logged in, first removed", State::LoggedIn, access_token_2, refresh_token_2, {"7", "8"}); - check_user("both logged in, first newer", State::LoggedIn, access_token_2, refresh_token_1, {"9", "10"}); - check_user("both logged in, second newer", State::LoggedIn, access_token_2, refresh_token_2, {"11", "12"}); + REQUIRE_FALSE(store->has_logged_in_user("removed user")); + check_user("first logged in, second logged out", access_token_1, refresh_token_1, {"1", "2"}); + check_user("first logged in, second removed", access_token_1, refresh_token_1, {"3", "4"}); + check_user("second logged in, first logged out", access_token_2, refresh_token_2, {"5", "6"}); + check_user("second logged in, first removed", access_token_2, refresh_token_2, {"7", "8"}); + check_user("both logged in, first newer", access_token_2, refresh_token_1, {"9", "10"}); + check_user("both logged in, second newer", access_token_2, refresh_token_2, {"11", "12"}); } } #endif // SWIFT_PACKAGE diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index ad2a3e6221c..dba6b020839 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -51,8 +51,8 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { auto& server = tsm.sync_server(); const std::string realm_base_url = server.base_url(); - auto check_for_sessions = [](SyncUser& user, size_t count, SyncSession::State state) { - auto sessions = user.all_sessions(); + auto check_for_sessions = [](TestUser& user, size_t count, SyncSession::State state) { + auto sessions = user.sync_manager()->get_all_sessions_for(user); CHECK(sessions.size() == count); for (auto& session : sessions) { CHECK(session->state() == state); @@ -69,9 +69,9 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { // Check the sessions on the SyncUser. check_for_sessions(*user, 2, SyncSession::State::Active); - auto s1 = user->session_for_on_disk_path(session1->path()); + auto s1 = tsm.sync_manager()->get_existing_session(session1->path()); REQUIRE(s1 == session1); - auto s2 = user->session_for_on_disk_path(session2->path()); + auto s2 = tsm.sync_manager()->get_existing_session(session2->path()); REQUIRE(s2 == session2); } @@ -88,7 +88,7 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { EventLoop::main().run_until([&] { return sessions_are_inactive(*session1, *session2); }); - check_for_sessions(*user, 0, SyncSession::State::Inactive); + check_for_sessions(*user, 2, SyncSession::State::Inactive); } SECTION("a SyncUser defers binding new sessions until it is logged in") { @@ -101,8 +101,9 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { spin_runloop(); REQUIRE(session1->state() == SyncSession::State::Inactive); REQUIRE(session2->state() == SyncSession::State::Inactive); - check_for_sessions(*user, 0, SyncSession::State::Inactive); - user->log_in(ENCODE_FAKE_JWT("fake_access_token"), ENCODE_FAKE_JWT("fake_refresh_token")); + check_for_sessions(*user, 2, SyncSession::State::Inactive); + // Log the user back in via the sync manager. + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session1, *session2); }); @@ -118,14 +119,16 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { return sessions_are_active(*session1, *session2); }); check_for_sessions(*user, 2, SyncSession::State::Active); + // Log the user out. user->log_out(); REQUIRE(user->state() == State::LoggedOut); // Run the runloop many iterations to see if the sessions spuriously rebind. spin_runloop(); REQUIRE(session1->state() == SyncSession::State::Inactive); REQUIRE(session2->state() == SyncSession::State::Inactive); - check_for_sessions(*user, 0, SyncSession::State::Inactive); - user->log_in(ENCODE_FAKE_JWT("fake_access_token"), ENCODE_FAKE_JWT("fake_refresh_token")); + check_for_sessions(*user, 2, SyncSession::State::Inactive); + // Log the user back in via the sync manager. + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session1, *session2); }); @@ -158,7 +161,7 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { auto session = sync_session( user, path, [](auto, auto) {}, SyncSessionStopPolicy::Immediately, &on_disk_path); CHECK(session); - session = user->session_for_on_disk_path(on_disk_path); + session = tsm.sync_manager()->get_existing_session(on_disk_path); CHECK(session); } @@ -427,7 +430,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { } SECTION("Properly handles a client reset error") { - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto session = sync_session(user, "/test", store_sync_error); std::string on_disk_path = session->path(); @@ -456,7 +460,9 @@ TEST_CASE("sync: error handling", "[sync][session]") { std::string recovery_path = error->user_info[SyncError::c_recovery_file_path_key]; auto idx = recovery_path.find("recovered_realm"); CHECK(idx != std::string::npos); - idx = recovery_path.find(tsm.sync_manager()->recovery_directory_path()); + idx = recovery_path.find(oas.app()->config().base_file_path); + CHECK(idx != std::string::npos); + idx = recovery_path.find(oas.app()->config().app_id); CHECK(idx != std::string::npos); if (just_before.tm_year == just_after.tm_year) { idx = recovery_path.find(util::format_local_time(just_after_raw, "%Y")); diff --git a/test/object-store/sync/session/wait_for_completion.cpp b/test/object-store/sync/session/wait_for_completion.cpp index ae2923b5eae..4ad9daa0bad 100644 --- a/test/object-store/sync/session/wait_for_completion.cpp +++ b/test/object-store/sync/session/wait_for_completion.cpp @@ -72,8 +72,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio spin_runloop(); REQUIRE(handler_called == false); // Log the user back in - user = sync_manager->get_user(user->identity(), ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), ""); + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); @@ -111,10 +110,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] if (!EventLoop::has_implementation()) return; - TestSyncManager::Config config; - config.should_teardown_test_directory = false; - SyncServer::Config server_config = {false}; - TestSyncManager tsm(config, server_config); + TestSyncManager tsm({}, {false}); auto& server = tsm.sync_server(); auto sync_manager = tsm.sync_manager(); std::atomic handler_called(false); @@ -154,8 +150,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] spin_runloop(); REQUIRE(handler_called == false); // Log the user back in - user = sync_manager->get_user(user->identity(), ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), ""); + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); diff --git a/test/object-store/sync/sync_manager.cpp b/test/object-store/sync/sync_manager.cpp index 57bb908cbc1..3aefafcf347 100644 --- a/test/object-store/sync/sync_manager.cpp +++ b/test/object-store/sync/sync_manager.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -34,63 +35,40 @@ using namespace realm; using namespace realm::util; using File = realm::util::File; +using MetadataMode = app::AppConfig::MetadataMode; static const auto base_path = fs::path{util::make_temp_dir()}.make_preferred() / "realm_objectstore_sync_manager.test-dir"; static const std::string dummy_device_id = "123400000000000000000000"; -namespace { -bool validate_user_in_vector(std::vector> vector, const std::string& identity, - const std::string& refresh_token, const std::string& access_token, - const std::string& device_id) -{ - for (auto& user : vector) { - if (user->identity() == identity && user->refresh_token() == refresh_token && - user->access_token() == access_token && user->has_device_id() && user->device_id() == device_id) { - return true; - } - } - return false; -} -} // anonymous namespace - -TEST_CASE("sync_manager: basic properties and APIs", "[sync][sync manager]") { - TestSyncManager tsm; - - SECTION("should not crash on 'reconnect()'") { - tsm.sync_manager()->reconnect(); - } -} - -TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { +TEST_CASE("App: path_for_realm API", "[sync][app][file]") { const std::string raw_url = "realms://realm.example.org/a/b/~/123456/xyz"; SECTION("should work properly") { - TestSyncManager tsm; - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto base_path = - fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->identity(); + fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->user_id(); const auto expected = base_path / "realms%3A%2F%2Frealm.example.org%2Fa%2Fb%2F%7E%2F123456%2Fxyz.realm"; - SyncConfig config(user, bson::Bson{}); - REQUIRE(tsm.sync_manager()->path_for_realm(config, raw_url) == expected); + SyncConfig sync_config(user, bson::Bson{}); + REQUIRE(oas.app()->path_for_realm(sync_config, raw_url) == expected); // This API should also generate the directory if it doesn't already exist. REQUIRE_DIR_PATH_EXISTS(base_path); } SECTION("should produce the expected path for all partition key types") { - TestSyncManager tsm; - auto sync_manager = tsm.sync_manager(); - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto base_path = - fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->identity(); - + fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->user_id(); + auto app = oas.app(); // Directory should not be created until we get the path REQUIRE_DIR_PATH_DOES_NOT_EXIST(base_path); SECTION("string") { const bson::Bson partition("string-partition-value&^#"); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "s_string-partition-value%26%5E%23.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "s_string-partition-value%26%5E%23.realm"); } SECTION("string which exceeds the file system path length limit") { @@ -100,9 +78,9 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { SyncConfig config(user, partition); // Note: does not include `identity` as that's in the hashed part - auto base_path = fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id"; + auto base_path = fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id"; const std::string expected_suffix = ".realm"; - std::string actual = sync_manager->path_for_realm(config); + std::string actual = oas.app()->path_for_realm(config); size_t expected_length = base_path.string().length() + 1 + 64 + expected_suffix.length(); REQUIRE(actual.length() == expected_length); REQUIRE(StringData(actual).begins_with(base_path.string())); @@ -112,66 +90,65 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { SECTION("int32") { const bson::Bson partition(int32_t(-25)); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "i_-25.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "i_-25.realm"); } SECTION("int64") { const bson::Bson partition(int64_t(1.15e18)); // > 32 bits SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "l_1150000000000000000.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "l_1150000000000000000.realm"); } SECTION("UUID") { const bson::Bson partition(UUID("3b241101-e2bb-4255-8caf-4136c566a961")); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == - base_path / "u_3b241101-e2bb-4255-8caf-4136c566a961.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "u_3b241101-e2bb-4255-8caf-4136c566a961.realm"); } SECTION("ObjectId") { const bson::Bson partition(ObjectId("0123456789abcdefffffffff")); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "o_0123456789abcdefffffffff.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "o_0123456789abcdefffffffff.realm"); } SECTION("Null") { const bson::Bson partition; REQUIRE(partition.type() == bson::Bson::Type::Null); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "null.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "null.realm"); } SECTION("Flexible sync") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "flx_sync_default.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "flx_sync_default.realm"); } SECTION("Custom filename for Flexible Sync") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom")) == base_path / "custom.realm"); } SECTION("Custom filename with type will still append .realm") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.foo")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.foo")) == base_path / "custom.foo.realm"); } SECTION("Custom filename for Flexible Sync including .realm") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.realm")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.realm")) == base_path / "custom.realm"); } SECTION("Custom filename for Flexible Sync with an existing path") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - std::string path = sync_manager->path_for_realm(config, util::make_optional("custom.realm")); + std::string path = app->path_for_realm(config, util::make_optional("custom.realm")); realm::test_util::TestPathGuard guard(path); realm::util::File existing_realm_file(path, File::mode_Write); existing_realm_file.write(std::string("test")); existing_realm_file.sync(); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.realm")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.realm")) == base_path / "custom.realm"); } @@ -180,527 +157,7 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { } } -TEST_CASE("sync_manager: user state management", "[sync][sync manager]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - - const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); - const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); - const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); - - const std::string a_token_1 = ENCODE_FAKE_JWT("wibble"); - const std::string a_token_2 = ENCODE_FAKE_JWT("wobble"); - const std::string a_token_3 = ENCODE_FAKE_JWT("wubble"); - - const std::string identity_1 = "user-foo"; - const std::string identity_2 = "user-bar"; - const std::string identity_3 = "user-baz"; - - SECTION("should get all users that are created during run time") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 2); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - } - - SECTION("should be able to distinguish users based solely on user ID") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_3, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); // existing - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_1, a_token_1, dummy_device_id)); - } - - SECTION("should properly update state in response to users logging in and out") { - auto r_token_3a = ENCODE_FAKE_JWT("qwerty"); - auto a_token_3a = ENCODE_FAKE_JWT("ytrewq"); - - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - auto u3 = sync_manager->get_user(identity_3, r_token_3, a_token_3, dummy_device_id); - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - // Log out users 1 and 3 - u1->log_out(); - u3->log_out(); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - // Log user 3 back in - u3 = sync_manager->get_user(identity_3, r_token_3a, a_token_3a, dummy_device_id); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); - // Log user 2 out - u2->log_out(); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); - } - - SECTION("should return current user that was created during run time") { - auto u_null = sync_manager->get_current_user(); - REQUIRE(u_null == nullptr); - - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - auto u_current = sync_manager->get_current_user(); - REQUIRE(u_current == u1); - - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - // The current user has switched to return the most recently used: "u2" - u_current = sync_manager->get_current_user(); - REQUIRE(u_current == u2); - } -} - -TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager]") { - TestSyncManager::Config config; - config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - TestSyncManager tsm(config); - config.base_path = tsm.base_file_path(); - config.should_teardown_test_directory = false; - auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); - const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); - const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); - const std::string a_token_1 = ENCODE_FAKE_JWT("wibble"); - const std::string a_token_2 = ENCODE_FAKE_JWT("wobble"); - const std::string a_token_3 = ENCODE_FAKE_JWT("wubble"); - - SECTION("when users are persisted") { - const std::string identity_1 = "foo-1"; - const std::string identity_2 = "bar-1"; - const std::string identity_3 = "baz-1"; - // First, create a few users and add them to the metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1); - u1->set_access_token(a_token_1); - u1->set_refresh_token(r_token_1); - u1->set_device_id(dummy_device_id); - auto u2 = manager.get_or_make_user_metadata(identity_2); - u2->set_access_token(a_token_2); - u2->set_refresh_token(r_token_2); - u2->set_device_id(dummy_device_id); - auto u3 = manager.get_or_make_user_metadata(identity_3); - u3->set_access_token(a_token_3); - u3->set_refresh_token(r_token_3); - u3->set_device_id(dummy_device_id); - // The fourth user is an "invalid" user: no token, so shouldn't show up. - auto u_invalid = manager.get_or_make_user_metadata("invalid_user"); - REQUIRE(manager.all_unmarked_users().size() == 4); - - SECTION("they should be added to the active users list when metadata is enabled") { - TestSyncManager tsm2(config); - auto users = tsm2.sync_manager()->all_users(); - REQUIRE(users.size() == 3); - REQUIRE(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - } - - SECTION("they should not be added to the active users list when metadata is disabled") { - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm2(config); - auto users = tsm2.sync_manager()->all_users(); - REQUIRE(users.size() == 0); - } - } - - struct TestPath { - bson::Bson partition; - std::string expected_path; - bool pre_create = true; - }; - std::vector dirs_to_create; - std::vector paths_under_test; - - SECTION("when users are marked") { - const std::string identity_1 = "foo-2"; - const std::string identity_2 = "bar-2"; - const std::string identity_3 = "baz-2"; - - // Create the user metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1); - auto u2 = manager.get_or_make_user_metadata(identity_2); - // Don't mark this user for deletion. - auto u3 = manager.get_or_make_user_metadata(identity_3); - - u1->set_legacy_identities({"legacy1"}); - u2->set_legacy_identities({"legacy2"}); - u3->set_legacy_identities({"legacy3"}); - - { - auto expected_u1_path = [&](const bson::Bson& partition) { - return ExpectedRealmPaths(tsm.base_file_path(), "app_id", u1->identity(), u1->legacy_identities(), - partition.to_string()); - }; - bson::Bson partition = "partition1"; - auto expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.current_preferred_path, false}); - - partition = "partition2"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.current_preferred_path, true}); - - partition = "partition3"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.fallback_hashed_path}); - - partition = "partition4"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.legacy_local_id_path}); - dirs_to_create.insert(dirs_to_create.end(), expected_paths.legacy_sync_directories_to_make.begin(), - expected_paths.legacy_sync_directories_to_make.end()); - - partition = "partition5"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.legacy_sync_path}); - dirs_to_create.insert(dirs_to_create.end(), expected_paths.legacy_sync_directories_to_make.begin(), - expected_paths.legacy_sync_directories_to_make.end()); - } - - std::vector paths; - { - auto sync_manager = tsm.sync_manager(); - - // Pre-populate the user directories. - auto user1 = sync_manager->get_user(u1->identity(), r_token_1, a_token_1, dummy_device_id); - auto user2 = sync_manager->get_user(u2->identity(), r_token_2, a_token_2, dummy_device_id); - auto user3 = sync_manager->get_user(u3->identity(), r_token_3, a_token_3, dummy_device_id); - for (auto& dir : dirs_to_create) { - try_make_dir(dir); - } - for (auto& test : paths_under_test) { - if (test.pre_create) { - create_dummy_realm(test.expected_path); - } - } - - paths = {sync_manager->path_for_realm(SyncConfig{user1, bson::Bson("123456789")}), - sync_manager->path_for_realm(SyncConfig{user1, bson::Bson("foo")}), - sync_manager->path_for_realm(SyncConfig{user2, bson::Bson("partition")}, {"123456789"}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("foo")}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("bar")}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("baz")})}; - - for (auto& test : paths_under_test) { - std::string actual = sync_manager->path_for_realm(SyncConfig{user1, test.partition}); - REQUIRE(actual == test.expected_path); - paths.push_back(actual); - } - - for (auto& path : paths) { - create_dummy_realm(path); - } - sync_manager->remove_user(u1->identity()); - sync_manager->remove_user(u2->identity()); - } - for (auto& path : paths) { - REQUIRE_REALM_EXISTS(path); - } - - config.should_teardown_test_directory = false; - SECTION("they should be cleaned up if metadata is enabled") { - TestSyncManager tsm(config); - auto users = tsm.sync_manager()->all_users(); - REQUIRE(users.size() == 1); - REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - REQUIRE_REALM_DOES_NOT_EXIST(paths[0]); - REQUIRE_REALM_DOES_NOT_EXIST(paths[1]); - REQUIRE_REALM_DOES_NOT_EXIST(paths[2]); - REQUIRE_REALM_EXISTS(paths[3]); - REQUIRE_REALM_EXISTS(paths[4]); - REQUIRE_REALM_EXISTS(paths[5]); - // all the remaining user 1 realms should have been deleted - for (size_t i = 6; i < paths.size(); ++i) { - REQUIRE_REALM_DOES_NOT_EXIST(paths[i]); - } - } - SECTION("they should be left alone if metadata is disabled") { - config.should_teardown_test_directory = true; - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - auto users = tsm.sync_manager()->all_users(); - for (auto& path : paths) { - REQUIRE_REALM_EXISTS(path); - } - } - } -} - -TEST_CASE("sync_manager: file actions", "[sync][sync manager]") { - test_util::TestDirGuard guard(base_path.string()); - - using Action = SyncFileActionMetadata::Action; - - auto file_manager = SyncFileManager(base_path.string(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - TestSyncManager::Config config; - config.base_path = base_path.string(); - config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - config.should_teardown_test_directory = false; - - const std::string realm_url = "https://example.realm.com/~/1"; - const std::string partition = "partition_foo"; - const std::string uuid_1 = "uuid-foo-1"; - const std::string uuid_2 = "uuid-bar-1"; - const std::string uuid_3 = "uuid-baz-1"; - const std::string uuid_4 = "uuid-baz-2"; - - const std::vector legacy_identities; - - // Realm paths - const std::string realm_path_1 = file_manager.realm_file_path(uuid_1, legacy_identities, realm_url, partition); - const std::string realm_path_2 = file_manager.realm_file_path(uuid_2, legacy_identities, realm_url, partition); - const std::string realm_path_3 = file_manager.realm_file_path(uuid_3, legacy_identities, realm_url, partition); - const std::string realm_path_4 = file_manager.realm_file_path(uuid_4, legacy_identities, realm_url, partition); - - // On windows you can't delete a realm if the file is open elsewhere. -#ifdef _WIN32 - SECTION("Action::DeleteRealm - fails if locked") { - SharedRealm locked_realm; - create_dummy_realm(realm_path_1, &locked_realm); - - REQUIRE(locked_realm); - - TestSyncManager tsm(config); - manager.make_file_action_metadata(realm_path_1, Action::DeleteRealm); - - REQUIRE_FALSE(tsm.sync_manager()->immediately_run_file_actions(realm_path_1)); - } -#endif - - SECTION("Action::DeleteRealm") { - - // Create some file actions - manager.make_file_action_metadata(realm_path_1, Action::DeleteRealm); - manager.make_file_action_metadata(realm_path_2, Action::DeleteRealm); - manager.make_file_action_metadata(realm_path_3, Action::DeleteRealm); - - SECTION("should properly delete the Realm") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // All Realms should be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - } - - SECTION("should fail gracefully if the Realm is missing") { - // Don't actually create the Realm files - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - TestSyncManager tsm(config); - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - } - - SECTION("should do nothing if metadata is disabled") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - // All file actions should still be present. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 3); - // All Realms should still be present. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_EXISTS(realm_path_2); - REQUIRE_REALM_EXISTS(realm_path_3); - } - } - - SECTION("Action::BackUpThenDeleteRealm") { - const auto recovery_dir = file_manager.recovery_directory_path(); - // Create some file actions - const std::string recovery_1 = util::file_path_by_appending_component(recovery_dir, "recovery-1"); - const std::string recovery_2 = util::file_path_by_appending_component(recovery_dir, "recovery-2"); - const std::string recovery_3 = util::file_path_by_appending_component(recovery_dir, "recovery-3"); - manager.make_file_action_metadata(realm_path_1, Action::BackUpThenDeleteRealm, recovery_1); - manager.make_file_action_metadata(realm_path_2, Action::BackUpThenDeleteRealm, recovery_2); - manager.make_file_action_metadata(realm_path_3, Action::BackUpThenDeleteRealm, recovery_3); - - SECTION("should properly copy the Realm file and delete the Realm") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // All Realms should be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_1)); - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should copy the Realm to the recovery_directory_path") { - const std::string identity = "b241922032489d4836ecd0c82d0445f0"; - const auto realm_base_path = file_manager.realm_file_path(identity, {}, "realmtasks", partition); - std::string recovery_path = util::reserve_unique_file_name( - file_manager.recovery_directory_path(), util::create_timestamped_template("recovered_realm")); - create_dummy_realm(realm_base_path); - REQUIRE_REALM_EXISTS(realm_base_path); - REQUIRE(!File::exists(recovery_path)); - // Manually create a file action metadata entry to simulate a client reset. - manager.make_file_action_metadata(realm_base_path, Action::BackUpThenDeleteRealm, recovery_path); - auto pending_actions = manager.all_pending_actions(); - REQUIRE(pending_actions.size() == 4); - - // Simulate client launch. - TestSyncManager tsm(config); - - CHECK(pending_actions.size() == 0); - CHECK(File::exists(recovery_path)); - REQUIRE_REALM_DOES_NOT_EXIST(realm_base_path); - } - - SECTION("should fail gracefully if the Realm is missing") { - // Don't actually create the Realm files - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // There should not be recovery files. - CHECK(!File::exists(recovery_1)); - CHECK(!File::exists(recovery_2)); - CHECK(!File::exists(recovery_3)); - } - - SECTION("should work properly when manually driven") { - REQUIRE(!File::exists(recovery_1)); - // Create a Realm file - create_dummy_realm(realm_path_4); - // Configure the system - TestSyncManager tsm(config); - REQUIRE(manager.all_pending_actions().size() == 0); - // Add a file action after the system is configured. - REQUIRE_REALM_EXISTS(realm_path_4); - REQUIRE(File::exists(file_manager.recovery_directory_path())); - manager.make_file_action_metadata(realm_path_4, Action::BackUpThenDeleteRealm, recovery_1); - REQUIRE(manager.all_pending_actions().size() == 1); - // Force the recovery. (In a real application, the user would have closed the files by now.) - REQUIRE(tsm.sync_manager()->immediately_run_file_actions(realm_path_4)); - // There should be recovery files. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_4); - CHECK(File::exists(recovery_1)); - REQUIRE(manager.all_pending_actions().size() == 0); - } - - SECTION("should fail gracefully if there is already a file at the destination") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - create_dummy_realm(recovery_1); - TestSyncManager tsm(config); - // Most file actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 1); - // Realms should be deleted. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should change the action to delete if copy succeeds but delete fails") { - if (!chmod_supported(base_path.string())) { - return; - } - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - // remove secondary files so the action doesn't throw when it can't read these - File::try_remove(DB::get_core_file(realm_path_3, DB::CoreFileType::Note)); - File::try_remove(DB::get_core_file(realm_path_3, DB::CoreFileType::Log)); - util::try_remove_dir_recursive(DB::get_core_file(realm_path_3, DB::CoreFileType::Management)); - // remove write permissions of the parent directory so that removing realm3 will fail - std::string realm3_dir = File::parent_dir(realm_path_3); - realm3_dir = realm3_dir.empty() ? "." : realm3_dir; - int original_perms = get_permissions(realm3_dir); - realm::chmod(realm3_dir, original_perms & (~0b010000000)); // without owner_write - // run the actions - TestSyncManager tsm(config); - // restore write permissions to the directory - realm::chmod(realm3_dir, original_perms); - // Everything succeeded except deleting realm_path_3 - auto pending_actions = manager.all_pending_actions(); - REQUIRE(pending_actions.size() == 1); - // the realm3 action changed from BackUpThenDeleteRealm to DeleteRealm - CHECK(pending_actions.get(0).action() == Action::DeleteRealm); - CHECK(pending_actions.get(0).original_name() == realm_path_3); - CHECK(File::exists(recovery_3)); // the copy was successful - CHECK(File::exists(realm_path_3)); // the delete failed - // try again with proper permissions - REQUIRE(tsm.sync_manager()->immediately_run_file_actions(realm_path_3)); - REQUIRE(manager.all_pending_actions().size() == 0); - // Realms should all be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should do nothing if metadata is disabled") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - // All file actions should still be present. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 3); - // All Realms should still be present. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_EXISTS(realm_path_2); - REQUIRE_REALM_EXISTS(realm_path_3); - // There should not be recovery files. - CHECK(!File::exists(recovery_1)); - CHECK(!File::exists(recovery_2)); - CHECK(!File::exists(recovery_3)); - } - } -} - -TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { +TEST_CASE("SyncManager: set_session_multiplexing", "[sync][sync manager]") { TestSyncManager::Config tsm_config; tsm_config.start_sync_client = false; TestSyncManager tsm(tsm_config); @@ -708,8 +165,8 @@ TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { auto sync_manager = tsm.sync_manager(); sync_manager->set_session_multiplexing(sync_multiplexing_allowed); - auto user_1 = tsm.fake_user("user 1"); - auto user_2 = tsm.fake_user("user 2"); + auto user_1 = tsm.fake_user("user-name-1"); + auto user_2 = tsm.fake_user("user-name-2"); SyncTestFile file_1(user_1, "partition1", util::none); SyncTestFile file_2(user_1, "partition2", util::none); @@ -734,9 +191,9 @@ TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { } } -TEST_CASE("sync_manager: has_existing_sessions", "[sync][sync manager][active sessions]") { - TestSyncManager init_sync_manager({}, {false}); - auto sync_manager = init_sync_manager.sync_manager(); +TEST_CASE("SyncManager: has_existing_sessions", "[sync][sync manager][active sessions]") { + TestSyncManager tsm({}, {false}); + auto sync_manager = tsm.sync_manager(); SECTION("no active sessions") { REQUIRE(!sync_manager->has_existing_sessions()); @@ -752,7 +209,7 @@ TEST_CASE("sync_manager: has_existing_sessions", "[sync][sync manager][active se std::atomic error_handler_invoked(false); Realm::Config config; - auto user = init_sync_manager.fake_user(); + auto user = tsm.fake_user("user-name"); auto create_session = [&](SyncSessionStopPolicy stop_policy) { std::shared_ptr session = sync_session( user, "/test-dying-state", diff --git a/test/object-store/sync/user.cpp b/test/object-store/sync/user.cpp deleted file mode 100644 index 57c0189d258..00000000000 --- a/test/object-store/sync/user.cpp +++ /dev/null @@ -1,303 +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 - -using namespace realm; -using namespace realm::util; -using File = realm::util::File; - -static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_user/"; -static const std::string dummy_device_id = "123400000000000000000000"; - -TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly creates a new normal user") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user); - // The expected state for a newly created user: - REQUIRE(user->identity() == identity); - REQUIRE(user->refresh_token() == refresh_token); - REQUIRE(user->access_token() == access_token); - REQUIRE(user->state() == SyncUser::State::LoggedIn); - } - - SECTION("properly retrieves a previously created user, updating fields as necessary") { - const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); - const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first); - REQUIRE(first->identity() == identity); - REQUIRE(first->refresh_token() == refresh_token); - // Get the user again, but with a different token. - auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); - REQUIRE(second == first); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == second_access_token); - REQUIRE(second->refresh_token() == second_refresh_token); - } - - SECTION("properly resurrects a logged-out user") { - const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); - const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - first->log_out(); - REQUIRE(first->state() == SyncUser::State::LoggedOut); - // Get the user again, with a new token. - auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); - REQUIRE(second == first); - REQUIRE(second->identity() == identity); - REQUIRE(second->refresh_token() == second_refresh_token); - REQUIRE(second->state() == SyncUser::State::LoggedIn); - } -} - -TEST_CASE("sync_user: update state and tokens", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("fake-access-token-1"); - const std::string second_refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-4"); - const std::string second_access_token = ENCODE_FAKE_JWT("fake-access-token-4"); - - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == refresh_token); - - user->log_in(second_access_token, second_refresh_token); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == second_refresh_token); - - user->log_out(); - REQUIRE(!user->is_logged_in()); - REQUIRE(user->refresh_token().empty()); - - user->log_in(access_token, refresh_token); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == refresh_token); - - user->invalidate(); -} - -TEST_CASE("sync_user: SyncManager get_existing_logged_in_user() API", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly returns a null pointer when called for a non-existent user") { - std::shared_ptr user = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(!user); - } - - SECTION("can get logged-in user from notification") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - REQUIRE(first->device_id() == dummy_device_id); - bool notification_fired = false; - auto sub_token = first->subscribe([&](const SyncUser& user) { - auto current_user = sync_manager->get_current_user(); - REQUIRE(current_user->identity() == identity); - REQUIRE(current_user->identity() == user.identity()); - notification_fired = true; - }); - - auto second = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - second->unsubscribe(sub_token); - REQUIRE(notification_fired); - } - - SECTION("properly returns an existing logged-in user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - REQUIRE(first->device_id() == dummy_device_id); - // Get that user using the 'existing user' API. - auto second = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(second == first); - REQUIRE(second->refresh_token() == refresh_token); - } - - SECTION("properly returns a null pointer for a logged-out user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - first->log_out(); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedOut); - // Get that user using the 'existing user' API. - auto second = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(!second); - } -} - -TEST_CASE("sync_user: logout", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly changes the state of the user object") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user->state() == SyncUser::State::LoggedIn); - user->log_out(); - REQUIRE(user->state() == SyncUser::State::LoggedOut); - } -} - -TEST_CASE("sync_user: user persistence", "[sync][user]") { - TestSyncManager::Config tsm_config; - tsm_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - TestSyncManager tsm(tsm_config); - auto sync_manager = tsm.sync_manager(); - auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - SECTION("properly persists a user's information upon creation") { - const std::string identity = "test_identity_1"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile(identities, {}); - // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE((bool)metadata); - REQUIRE(metadata->is_valid()); - REQUIRE(metadata->access_token() == access_token); - REQUIRE(metadata->refresh_token() == refresh_token); - REQUIRE(metadata->device_id() == dummy_device_id); - REQUIRE(metadata->identities() == identities); - } - - SECTION("properly removes a user's access/refresh token upon log out") { - const std::string identity = "test_identity_1"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile(identities, {}); - user->log_out(); - // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE((bool)metadata); - REQUIRE(metadata->is_valid()); - REQUIRE(metadata->access_token() == ""); - REQUIRE(metadata->refresh_token() == ""); - REQUIRE(metadata->device_id() == dummy_device_id); - REQUIRE(metadata->identities() == identities); - REQUIRE(metadata->state() == SyncUser::State::LoggedOut); - REQUIRE(user->is_logged_in() == false); - } - - SECTION("properly persists a user's information when the user is updated") { - const std::string identity = "test_identity_2"; - const std::string refresh_token = ENCODE_FAKE_JWT("r_token-2a"); - const std::string access_token = ENCODE_FAKE_JWT("a_token-1a"); - // Create the user and validate it. - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - auto first_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(first_metadata->is_valid()); - REQUIRE(first_metadata->access_token() == access_token); - const std::string token_2 = ENCODE_FAKE_JWT("token-2b"); - // Update the user. - auto second = sync_manager->get_user(identity, refresh_token, token_2, dummy_device_id); - auto second_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second_metadata->is_valid()); - REQUIRE(second_metadata->access_token() == token_2); - } - - SECTION("properly does not mark a user when the user is logged out and not anon") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Log out the user. - user->log_out(); - marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - } - - SECTION("properly removes a user when the user is logged out and is anon") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile({{"id", app::IdentityProviderAnonymous}}, {}); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Log out the user. - user->log_out(); - REQUIRE(sync_manager->all_users().size() == 0); - } - - SECTION("properly revives a logged-out user when it's requested again") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-4a"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-4a"); - // Create the user and log it out. - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - first->log_out(); - REQUIRE(sync_manager->all_users().size() == 1); - REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedOut); - // Log the user back in. - const std::string r_token_2 = ENCODE_FAKE_JWT("r-token-4b"); - const std::string a_token_2 = ENCODE_FAKE_JWT("atoken-4b"); - auto second = sync_manager->get_user(identity, r_token_2, a_token_2, dummy_device_id); - REQUIRE(sync_manager->all_users().size() == 1); - REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedIn); - } - - SECTION("properly deletes a user") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - sync_manager->set_current_user(identity); - REQUIRE(sync_manager->get_current_user() == user); - REQUIRE(sync_manager->all_users().size() == 1); - sync_manager->delete_user(user->identity()); - REQUIRE(sync_manager->all_users().size() == 0); - REQUIRE(sync_manager->get_current_user() == nullptr); - } -} diff --git a/test/object-store/util/sync/baas_admin_api.hpp b/test/object-store/util/sync/baas_admin_api.hpp index ce85d8ecc22..35aa8402de9 100644 --- a/test/object-store/util/sync/baas_admin_api.hpp +++ b/test/object-store/util/sync/baas_admin_api.hpp @@ -291,7 +291,7 @@ AppSession get_runtime_app_session(); std::string get_mongodb_server(); template -inline app::App::Config get_config(Factory factory, const AppSession& app_session) +inline app::AppConfig get_config(Factory factory, const AppSession& app_session) { return {app_session.client_app_id, factory, diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index 52fd33d6026..22ae3b42c79 100644 --- a/test/object-store/util/sync/sync_test_utils.cpp +++ b/test/object-store/util/sync/sync_test_utils.cpp @@ -52,27 +52,6 @@ std::ostream& operator<<(std::ostream& os, util::Optional error) return os; } -bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity) -{ - for (size_t i = 0; i < results.size(); i++) { - auto this_result = results.get(i); - if (this_result.identity() == identity) { - return true; - } - } - return false; -} - -bool results_contains_original_name(SyncFileActionMetadataResults& results, const std::string& original_name) -{ - for (size_t i = 0; i < results.size(); i++) { - if (results.get(i).original_name() == original_name) { - return true; - } - } - return false; -} - bool ReturnsTrueWithinTimeLimit::match(util::FunctionRef condition) const { const auto wait_start = std::chrono::steady_clock::now(); @@ -315,7 +294,6 @@ void async_open_realm(const Realm::Config& config, task->cancel(); // don't run the above notifier again on this session finish(std::move(tsr), err); } - #endif // REALM_ENABLE_SYNC class TestHelper { @@ -459,7 +437,7 @@ struct FakeLocalClientReset : public TestClientReset { #if REALM_ENABLE_AUTH_TESTS -void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, +void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, const std::string& schema_name, const bson::BsonDocument& filter_bson) { // While at this point the object has been sync'd successfully, we must also @@ -490,7 +468,7 @@ void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const A std::chrono::minutes(15), std::chrono::milliseconds(500)); } -void wait_for_num_objects_in_atlas(std::shared_ptr user, const AppSession& app_session, +void wait_for_num_objects_in_atlas(std::shared_ptr user, const AppSession& app_session, const std::string& schema_name, size_t expected_size) { app::MongoClient remote_client = user->mongo_client("BackingDB"); diff --git a/test/object-store/util/sync/sync_test_utils.hpp b/test/object-store/util/sync/sync_test_utils.hpp index 9649f7f3517..db79fa5bfbc 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 @@ -46,9 +46,6 @@ namespace realm { -bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity); -bool results_contains_original_name(SyncFileActionMetadataResults& results, const std::string& original_name); - void timed_wait_for(util::FunctionRef condition, std::chrono::milliseconds max_ms = std::chrono::milliseconds(5000)); @@ -137,6 +134,14 @@ std::ostream& operator<<(std::ostream& os, util::Optional error); void subscribe_to_all_and_bootstrap(Realm& realm); +struct AutoVerifiedEmailCredentials : app::AppCredentials { + AutoVerifiedEmailCredentials(); + std::string email; + std::string password; +}; + +AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); + #if REALM_ENABLE_AUTH_TESTS void wait_for_sessions_to_close(const TestAppSession& test_app_session); @@ -146,14 +151,6 @@ std::string get_admin_url(); #endif #endif // REALM_ENABLE_AUTH_TESTS -struct AutoVerifiedEmailCredentials : app::AppCredentials { - AutoVerifiedEmailCredentials(); - std::string email; - std::string password; -}; - -AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); - void wait_for_advance(Realm& realm); void async_open_realm(const Realm::Config& config, @@ -217,10 +214,10 @@ std::unique_ptr make_baas_flx_client_reset(const Realm::Config& const Realm::Config& remote_config, const TestAppSession& test_app_session); -void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, +void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, const std::string& schema_name, const bson::BsonDocument& filter_bson); -void wait_for_num_objects_in_atlas(std::shared_ptr user, const AppSession& app_session, +void wait_for_num_objects_in_atlas(std::shared_ptr user, const AppSession& app_session, const std::string& schema_name, size_t expected_size); void trigger_client_reset(const AppSession& app_session, const SyncSession& sync_session); diff --git a/test/object-store/util/test_file.cpp b/test/object-store/util/test_file.cpp index c4f82dfe266..95af03979c5 100644 --- a/test/object-store/util/test_file.cpp +++ b/test/object-store/util/test_file.cpp @@ -20,8 +20,10 @@ #include "util/test_utils.hpp" #include "util/sync/baas_admin_api.hpp" +#include "util/sync/sync_test_utils.hpp" #include "../util/crypt_key.hpp" #include "../util/test_path.hpp" +#include "util/sync/sync_test_utils.hpp" #include #include @@ -183,11 +185,6 @@ static const std::string fake_refresh_token = ENCODE_FAKE_JWT("not_a_real_token" static const std::string fake_access_token = ENCODE_FAKE_JWT("also_not_real"); static const std::string fake_device_id = "123400000000000000000000"; -static std::shared_ptr get_fake_user(SyncManager& sync_manager, const std::string& user_name) -{ - return sync_manager.get_user(user_name, fake_refresh_token, fake_access_token, fake_device_id); -} - SyncTestFile::SyncTestFile(TestSyncManager& tsm, std::string name, std::string user_name) : SyncTestFile(tsm.fake_user(user_name), bson::Bson(name)) { @@ -242,8 +239,8 @@ SyncTestFile::SyncTestFile(std::shared_ptr user, realm::Schema schema_mode = SchemaMode::AdditiveExplicit; } -SyncTestFile::SyncTestFile(std::shared_ptr app, bson::Bson partition, Schema schema) - : SyncTestFile(app->current_user(), std::move(partition), std::move(schema)) +SyncTestFile::SyncTestFile(TestSyncManager& tsm, bson::Bson partition, Schema schema) + : SyncTestFile(tsm.fake_user("test"), std::move(partition), std::move(schema)) { } @@ -317,7 +314,7 @@ static Status wait_for_session(Realm& realm, void (SyncSession::*fn)(util::Uniqu std::chrono::seconds timeout) { auto shared_state = std::make_shared(); - auto& session = *realm.config().sync_config->user->session_for_on_disk_path(realm.config().path); + auto& session = *realm.sync_session(); auto delay = TEST_TIMEOUT_EXTRA > 0 ? timeout + std::chrono::seconds(TEST_TIMEOUT_EXTRA) : timeout; (session.*fn)([weak_state = std::weak_ptr(shared_state)](Status s) { auto shared_state = weak_state.lock(); @@ -349,7 +346,7 @@ bool wait_for_download(Realm& realm, std::chrono::seconds timeout) return !wait_for_session(realm, &SyncSession::wait_for_download_completion, timeout).is_ok(); } -void set_app_config_defaults(app::App::Config& app_config, +void set_app_config_defaults(app::AppConfig& app_config, const std::shared_ptr& transport) { if (!app_config.transport) @@ -372,6 +369,7 @@ void set_app_config_defaults(app::App::Config& app_config, app_config.device_info.bundle_id = "Bundle Id"; if (app_config.app_id.empty()) app_config.app_id = "app_id"; + app_config.metadata_mode = app::AppConfig::MetadataMode::InMemory; } // MARK: - TestAppSession @@ -397,19 +395,18 @@ TestAppSession::TestAppSession(AppSession session, auto app_config = get_config(m_transport, *m_app_session); set_default_level_thresholds(); set_app_config_defaults(app_config, m_transport); + app_config.base_file_path = m_base_file_path; + app_config.metadata_mode = realm::app::AppConfig::MetadataMode::NoEncryption; util::try_make_dir(m_base_file_path); - SyncClientConfig sc_config; - sc_config.base_file_path = m_base_file_path; - sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoEncryption; - sc_config.reconnect_mode = reconnect_mode; - sc_config.socket_provider = custom_socket_provider; + app_config.sync_client_config.reconnect_mode = reconnect_mode; + app_config.sync_client_config.socket_provider = custom_socket_provider; // With multiplexing enabled, the linger time controls how long a // connection is kept open for reuse. In tests, we want to shut // down sync clients immediately. - sc_config.timeouts.connection_linger_time = 0; + app_config.sync_client_config.timeouts.connection_linger_time = 0; - m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config); // initialize sync client m_app->sync_manager()->get_sync_client(); @@ -433,7 +430,7 @@ TestAppSession::~TestAppSession() } } -std::vector TestAppSession::get_documents(SyncUser& user, const std::string& object_type, +std::vector TestAppSession::get_documents(app::User& user, const std::string& object_type, size_t expected_count) const { app::MongoClient remote_client = user.mongo_client("BackingDB"); @@ -483,16 +480,14 @@ TestSyncManager::Config::Config() } TestSyncManager::TestSyncManager(const Config& config, const SyncServer::Config& sync_server_config) - : m_sync_server(sync_server_config) + : m_sync_manager(SyncManager::create(SyncClientConfig())) + , m_sync_server(sync_server_config) , m_base_file_path(config.base_path.empty() ? util::make_temp_dir() : config.base_path) , m_should_teardown_test_directory(config.should_teardown_test_directory) { util::try_make_dir(m_base_file_path); - SyncClientConfig sc_config; - sc_config.base_file_path = m_base_file_path; - sc_config.metadata_mode = config.metadata_mode; - m_sync_manager = SyncManager::create(nullptr, m_sync_server.base_url() + "/realm-sync", sc_config, "app_id"); + m_sync_manager->set_sync_route(m_sync_server.base_url() + "/realm-sync"); if (config.start_sync_client) { m_sync_manager->get_sync_client(); } @@ -514,9 +509,12 @@ TestSyncManager::~TestSyncManager() } } -std::shared_ptr TestSyncManager::fake_user(const std::string& name) +std::shared_ptr TestSyncManager::fake_user(const std::string& name) { - return get_fake_user(*m_sync_manager, name); + auto user = std::make_shared(name, m_sync_manager); + user->m_access_token = fake_access_token; + user->m_refresh_token = fake_refresh_token; + return user; } OfflineAppSession::Config::Config(std::shared_ptr t) @@ -529,6 +527,11 @@ OfflineAppSession::OfflineAppSession(OfflineAppSession::Config config) , m_delete_storage(config.delete_storage) { REALM_ASSERT(m_transport); + app::AppConfig app_config; + set_app_config_defaults(app_config, m_transport); + + set_default_level_thresholds(); + if (config.storage_path) { m_base_file_path = *config.storage_path; util::try_make_dir(m_base_file_path); @@ -537,23 +540,16 @@ OfflineAppSession::OfflineAppSession(OfflineAppSession::Config config) m_base_file_path = util::make_temp_dir(); } - app::App::Config app_config; - set_app_config_defaults(app_config, m_transport); + app_config.base_file_path = m_base_file_path; + app_config.metadata_mode = config.metadata_mode; if (config.base_url) { app_config.base_url = *config.base_url; } if (config.app_id) { app_config.app_id = *config.app_id; } - - SyncClientConfig sc_config; - sc_config.base_file_path = m_base_file_path; - sc_config.metadata_mode = config.metadata_mode; - sc_config.socket_provider = config.socket_provider; - - set_default_level_thresholds(); - - m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + app_config.sync_client_config.socket_provider = config.socket_provider; + m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config); } OfflineAppSession::~OfflineAppSession() @@ -570,9 +566,10 @@ OfflineAppSession::~OfflineAppSession() } } -std::shared_ptr OfflineAppSession::make_user() const +std::shared_ptr OfflineAppSession::make_user() const { - return get_fake_user(*m_app->sync_manager(), "test user"); + create_user_and_log_in(app()); + return app()->current_user(); } #endif // REALM_ENABLE_SYNC diff --git a/test/object-store/util/test_file.hpp b/test/object-store/util/test_file.hpp index 26f7f23a56e..5be27960570 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -168,6 +168,75 @@ class SyncServer : private realm::sync::Clock { } }; +struct TestUser : realm::SyncUser { + const std::string m_user_id; + std::string m_access_token; + std::string m_refresh_token; + std::shared_ptr m_sync_manager; + realm::SyncUser::State m_state = realm::SyncUser::State::LoggedIn; + + TestUser(std::string user_id, std::shared_ptr sync_manager) + : m_user_id(std::move(user_id)) + , m_sync_manager(std::move(sync_manager)) + { + } + + void log_out() + { + auto old_state = m_state; + m_state = realm::SyncUser::State::LoggedOut; + m_sync_manager->update_sessions_for(*this, old_state, m_state, {}); + } + + void log_in() + { + auto old_state = m_state; + m_state = realm::SyncUser::State::LoggedIn; + m_sync_manager->update_sessions_for(*this, old_state, m_state, m_access_token); + } + + std::string user_id() const noexcept override + { + return m_user_id; + } + std::string app_id() const noexcept override + { + return "app id"; + } + + std::string access_token() const override + { + return m_access_token; + } + std::string refresh_token() const override + { + return m_access_token; + } + realm::SyncUser::State state() const override + { + return m_state; + } + bool access_token_refresh_required() const override + { + return false; + } + realm::SyncManager* sync_manager() override + { + return m_sync_manager.get(); + } + + void request_log_out() override {} + void request_refresh_user(CompletionHandler&&) override {} + void request_refresh_location(CompletionHandler&&) override {} + void request_access_token(CompletionHandler&&) override {} + + void track_realm(std::string_view) override {} + std::string create_file_action(realm::SyncFileAction, std::string_view, std::optional) override + { + return ""; + } +}; + class OfflineAppSession; struct SyncTestFile : TestFile { template @@ -187,7 +256,7 @@ struct SyncTestFile : TestFile { SyncTestFile(std::shared_ptr user, realm::bson::Bson partition, realm::util::Optional schema, std::function&& error_handler); - SyncTestFile(std::shared_ptr app, realm::bson::Bson partition, realm::Schema schema); + SyncTestFile(TestSyncManager&, realm::bson::Bson partition, realm::Schema schema); SyncTestFile(std::shared_ptr user, realm::Schema schema, realm::SyncConfig::FLXSyncEnabled); }; @@ -196,7 +265,6 @@ class TestSyncManager { struct Config { Config(); std::string base_path; - realm::SyncManager::MetadataMode metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; bool should_teardown_test_directory = true; realm::util::Logger::Level log_level = realm::util::Logger::Level::TEST_LOGGING_LEVEL; bool start_sync_client = true; @@ -218,13 +286,13 @@ class TestSyncManager { return m_sync_manager; } - std::shared_ptr fake_user(const std::string& name = "test"); + std::shared_ptr fake_user(const std::string& name = "test"); private: std::shared_ptr m_sync_manager; SyncServer m_sync_server; - std::string m_base_file_path; - bool m_should_teardown_test_directory = true; + const std::string m_base_file_path; + const bool m_should_teardown_test_directory = true; }; class OfflineAppSession { @@ -234,7 +302,7 @@ class OfflineAppSession { std::shared_ptr transport; bool delete_storage = true; std::optional storage_path; - realm::SyncManager::MetadataMode metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; + realm::app::AppConfig::MetadataMode metadata_mode = realm::app::AppConfig::MetadataMode::InMemory; std::optional base_url; std::shared_ptr socket_provider; std::optional app_id; @@ -246,7 +314,7 @@ class OfflineAppSession { { return m_app; } - std::shared_ptr make_user() const; + std::shared_ptr make_user() const; realm::app::GenericNetworkTransport* transport() { return m_transport.get(); @@ -294,7 +362,7 @@ class TestAppSession { return m_app->sync_manager(); } - std::vector get_documents(realm::SyncUser& user, const std::string& object_type, + std::vector get_documents(realm::app::User& user, const std::string& object_type, size_t expected_count) const; private: @@ -310,7 +378,7 @@ class TestAppSession { bool wait_for_upload(realm::Realm& realm, std::chrono::seconds timeout = std::chrono::seconds(60)); bool wait_for_download(realm::Realm& realm, std::chrono::seconds timeout = std::chrono::seconds(60)); -void set_app_config_defaults(realm::app::App::Config& app_config, +void set_app_config_defaults(realm::app::AppConfig& app_config, const std::shared_ptr& transport); #endif // REALM_ENABLE_SYNC diff --git a/test/object-store/util/unit_test_transport.cpp b/test/object-store/util/unit_test_transport.cpp index 926ea8afc76..f9174deeaa2 100644 --- a/test/object-store/util/unit_test_transport.cpp +++ b/test/object-store/util/unit_test_transport.cpp @@ -30,7 +30,7 @@ using namespace realm; using namespace realm::app; -std::string UnitTestTransport::access_token = encode_fake_jwt("fake access token"); +static const std::string access_token = encode_fake_jwt("fake access token"); const std::string UnitTestTransport::api_key = "lVRPQVYBJSIbGos2ZZn0mGaIq1SIOsGaZ5lrcp8bxlR5jg4OGuGwQq1GkektNQ3i"; const std::string UnitTestTransport::api_key_id = "5e5e6f0abe4ae2a2c2c2d329"; const std::string UnitTestTransport::api_key_name = "some_api_key_name"; diff --git a/test/object-store/util/unit_test_transport.hpp b/test/object-store/util/unit_test_transport.hpp index 509e4187275..5cf6b4dd835 100644 --- a/test/object-store/util/unit_test_transport.hpp +++ b/test/object-store/util/unit_test_transport.hpp @@ -36,8 +36,6 @@ class UnitTestTransport : public realm::app::GenericNetworkTransport { { } - static std::string access_token; - static const std::string api_key; static const std::string api_key_id; static const std::string api_key_name;