diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f4844b05a..0ac1ac4c697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,33 @@ * (PR [#????](https://github.com/realm/realm-core/pull/????)) * Add `SyncClientConfig::security_access_group` which allows specifying the access group to use for the sync metadata Realm's encryption key. Setting this is required when sharing the metadata Realm between apps on Apple platforms ([#7552](https://github.com/realm/realm-core/pull/7552)). * When connecting to multiple server apps, a unique encryption key is used for each of the metadata Realms rather than sharing one between them ([#7552](https://github.com/realm/realm-core/pull/7552)). +* 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). +* App::switch_user() no longer returns a user. The return value was always exactly the passed-in user and any code which needs it can just use that. ([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. @@ -18,7 +38,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). ---------------------------------------------- @@ -272,7 +294,7 @@ * Fixed a crash with `Assertion failed: m_initiated` during sync session startup ([#7074](https://github.com/realm/realm-core/issues/7074), since v10.0.0). * Fixed a TSAN violation where the user thread could race to read `m_finalized` with the sync event loop ([#6844](https://github.com/realm/realm-core/issues/6844), since v13.15.1) * Fix a minor race condition when backing up Realm files before a client reset which could have lead to overwriting an existing file. ([PR #7341](https://github.com/realm/realm-core/pull/7341)). - + ### 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)) diff --git a/bindgen/spec.yml b/bindgen/spec.yml index 0d54e00d098..76284e08fa7 100644 --- a/bindgen/spec.yml +++ b/bindgen/spec.yml @@ -227,7 +227,7 @@ enums: - RecoverOrDiscard MetadataMode: - cppName: SyncClientConfig::MetadataMode + cppName: app::AppConfig::MetadataMode values: - NoEncryption - Encryption @@ -288,6 +288,12 @@ enums: - ClientReset - ClientResetNoRecovery + SyncFileAction: + cppName: SyncFileAction + values: + - DeleteRealm + - BackUpThenDeleteRealm + ProgressDirection: cppName: SyncSession::ProgressDirection values: @@ -295,7 +301,7 @@ enums: - download SyncUserState: - cppName: SyncUser::State + cppName: UserState values: - LoggedOut - LoggedIn @@ -451,7 +457,7 @@ records: default: false UserIdentity: - cppName: SyncUserIdentity + cppName: app::UserIdentity fields: id: type: std::string @@ -593,14 +599,6 @@ records: SyncClientConfig: fields: - base_file_path: std::string - metadata_mode: - type: MetadataMode - default: MetadataMode::Encryption - custom_encryption_key: std::optional - security_access_group: - type: std::string - default: "" logger_factory: Nullable log_level: type: LoggerLevel @@ -659,7 +657,7 @@ records: body: std::string DeviceInfo: - cppName: app::App::Config::DeviceInfo + cppName: app::AppConfig::DeviceInfo fields: platform_version: std::string sdk_version: std::string @@ -671,13 +669,22 @@ 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 + security_access_group: + type: std::string + default: "" CompensatingWriteErrorInfo: cppName: sync::CompensatingWriteErrorInfo @@ -1193,36 +1200,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' @@ -1243,27 +1263,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 @@ -1273,8 +1293,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' @@ -1291,8 +1311,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 @@ -1308,12 +1328,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 @@ -1330,7 +1350,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)' @@ -1338,8 +1357,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 4fc95d1e1ba..8aa6dc1b92d 100644 --- a/src/realm.h +++ b/src/realm.h @@ -2883,6 +2883,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; @@ -2992,6 +2998,13 @@ 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 void realm_app_config_set_security_access_group(realm_app_config_t*, const char*) 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 @@ -3002,18 +3015,18 @@ RLM_API void realm_app_config_set_bundle_id(realm_app_config_t* config, const ch RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credentials_t*) RLM_API_NOEXCEPT; /** - * Create realm_app_t* instance given a valid realm configuration and sync client configuration. + * Create realm_app_t* instance given a valid realm app configuration. * * @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. + * Create cached realm_app_t* instance given a valid realm app 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 @@ -3143,11 +3156,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. @@ -3382,9 +3394,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 @@ -3460,12 +3472,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, @@ -3601,7 +3607,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. @@ -3627,11 +3633,6 @@ typedef void (*realm_async_open_task_init_subscription_func_t)(realm_thread_safe 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; @@ -3660,8 +3661,6 @@ RLM_API void realm_sync_client_config_set_default_binding_thread_observer( realm_sync_client_config_t* config, realm_on_object_store_thread_callback_t on_thread_create, realm_on_object_store_thread_callback_t on_thread_destroy, realm_on_object_store_error_callback_t on_error, realm_userdata_t user_data, realm_free_userdata_func_t free_userdata); -RLM_API void realm_sync_client_config_set_security_access_group(realm_sync_client_config_t*, - const char*) RLM_API_NOEXCEPT; RLM_API realm_sync_config_t* realm_sync_config_new(const realm_user_t*, const char* partition_value) RLM_API_NOEXCEPT; RLM_API realm_sync_config_t* realm_flx_sync_config_new(const realm_user_t*) RLM_API_NOEXCEPT; @@ -4018,7 +4017,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 d32b334b65a..67c82a0cada 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -548,6 +548,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 06888fad477..a9ae327f937 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); @@ -243,6 +243,27 @@ 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 void realm_app_config_set_security_access_group(realm_app_config_t* config, const char* group) noexcept +{ + config->security_access_group = group; +} + RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credentials_t* app_credentials) noexcept { return wrap_err([&] { @@ -250,19 +271,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)); }); } @@ -286,19 +305,15 @@ RLM_API void realm_clear_cached_apps(void) noexcept RLM_API const char* realm_app_get_app_id(const realm_app_t* app) noexcept { - return (*app)->config().app_id.c_str(); + return (*app)->app_id().c_str(); } RLM_API bool realm_app_update_base_url(realm_app_t* app, const char* base_url, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - std::optional new_base_url; - if (base_url) { - new_base_url = base_url; - } return wrap_err([&] { - (*app)->update_base_url(new_base_url, make_callback(callback, userdata, userdata_free)); + (*app)->update_base_url(base_url ? base_url : "", make_callback(callback, userdata, userdata_free)); return true; }); } @@ -353,22 +368,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)); }); } @@ -376,38 +409,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)); }); } @@ -501,10 +527,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)); }); } @@ -514,10 +539,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)); }); } @@ -526,7 +550,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) { @@ -543,8 +567,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)); }); } @@ -554,10 +577,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)); }); } @@ -567,10 +589,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)); }); } @@ -580,10 +601,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)); }); } @@ -591,11 +611,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)); }); } @@ -605,11 +624,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)); }); } @@ -618,7 +636,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) { @@ -631,8 +649,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)); }); } @@ -654,17 +671,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 @@ -675,8 +696,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++) { @@ -684,24 +705,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(); }); } @@ -712,20 +733,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) @@ -745,39 +767,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(); @@ -786,13 +804,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); @@ -801,7 +819,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; @@ -812,8 +830,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); @@ -824,6 +842,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) @@ -832,8 +851,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 83fab432df2..7acc081bd72 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 { @@ -230,13 +212,6 @@ RLM_API void realm_sync_client_config_set_resumption_delay_backoff_multiplier(re config->timeouts.reconnect_backoff_info.resumption_delay_backoff_multiplier = multiplier; } -RLM_API void realm_sync_client_config_set_security_access_group(realm_sync_client_config_t* config, - const char* group) noexcept -{ - config->security_access_group = group; -} - - /// Register an app local callback handler for bindings interested in registering callbacks before/after /// the ObjectStore thread runs for this app. This only works for the default socket provider implementation. /// IMPORTANT: If a function is supplied that handles the exception, it must call abort() or cause the @@ -801,7 +776,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(); @@ -540,8 +545,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 85c33710730..08d42aeb3f2 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,14 +23,17 @@ #include #include #include +#include #include +#include +#include #include -#include #ifdef __EMSCRIPTEN__ #include #endif +#include #include #include @@ -135,25 +137,20 @@ struct UserAPIKeyResponseHandler { } }; -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, - RequestTokenType token_type = RequestTokenType::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& user, RequestTokenType token_type) { HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}}; - - if (with_user_authorization) { + if (user) { switch (token_type) { case RequestTokenType::NoAuth: break; case RequestTokenType::AccessToken: - headers.insert({"Authorization", util::format("Bearer %1", with_user_authorization->access_token())}); + headers.insert({"Authorization", util::format("Bearer %1", user->access_token())}); break; case RequestTokenType::RefreshToken: - headers.insert( - {"Authorization", util::format("Bearer %1", with_user_authorization->refresh_token())}); + headers.insert({"Authorization", util::format("Bearer %1", user->refresh_token())}); break; } } @@ -177,64 +174,35 @@ 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 { +namespace realm::app { std::string_view App::default_base_url() { return "https://services.cloud.mongodb.com"; } -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(App::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; @@ -249,13 +217,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(); @@ -263,11 +231,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(App::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) { @@ -275,18 +245,16 @@ 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) { - if (auto comp = AppUtils::split_url(*m_config.base_url); !comp.is_ok()) { - throw Exception(comp.get_status()); - } + util::Uri::parse(*m_config.base_url); } // Setup a baseline set of routes using the provided or default base url // These will be updated when the location info is refreshed prior to sending the // first AppServices HTTP request. - configure_route(m_base_url); + configure_route(m_base_url, ""); + m_sync_manager->set_sync_route(make_sync_route(), false); if (m_config.device_info.platform_version.empty()) { throw InvalidArgument("You must specify the Platform Version in App::Config::device_info"); @@ -303,29 +271,13 @@ App::App(Private, const Config& config) App::~App() {} -void App::configure(const SyncClientConfig& sync_client_config) -{ - std::string ws_route; - { - util::CheckedLockGuard guard(m_route_mutex); - // Make sure to request the location when the app is configured - m_location_updated = false; - // Create a tentative sync route using the generated ws_host_url - REALM_ASSERT(!m_ws_host_url.empty()); - ws_route = make_sync_route(); - } - - // When App starts, the ws_host_url will be populated with the generated value based on - // the provided host_url value and the sync route will be created using this. If this is - // is incorrect, the websocket connection will fail and the SyncSession will request a - // new access token, which will update the location if it has not already. - m_sync_manager = SyncManager::create(shared_from_this(), ws_route, 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); } @@ -377,26 +329,18 @@ std::string App::get_ws_host_url() return m_ws_host_url; } - std::string App::make_sync_route(Optional ws_host_url) { return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id, s_sync_path); } -void App::configure_route(const std::string& host_url, const std::optional& ws_host_url) +void App::configure_route(const std::string& host_url, const std::string& ws_host_url) { - // We got a new host url, save it - m_host_url = (host_url.length() > 0 ? host_url : m_base_url); - - // If a valid websocket host url was included, save it - if (ws_host_url && ws_host_url->length() > 0) { - m_ws_host_url = *ws_host_url; - } - // Otherwise, convert the host url to a websocket host url - else { + m_host_url = host_url; + m_ws_host_url = ws_host_url; + if (m_ws_host_url.empty()) m_ws_host_url = App::create_ws_host_url(m_host_url); - } // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090 // base_route is the baseline client api path: e.g. /api/client/v2.0 @@ -413,7 +357,7 @@ void App::configure_route(const std::string& host_url, const std::optional ws[s]://ws.[region-prefix]realm.mongodb.com // http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com // All others => http[s]:// => ws[s]:// -std::string App::create_ws_host_url(const std::string_view host_url) +std::string App::create_ws_host_url(std::string_view host_url) { constexpr static std::string_view old_base_domain = "realm.mongodb.com"; constexpr static std::string_view new_base_domain = "services.cloud.mongodb.com"; @@ -445,19 +389,15 @@ std::string App::create_ws_host_url(const std::string_view host_url) return util::format("ws%1", host_url.substr(4)); } -void App::update_hostname(const std::string& host_url, const std::optional& ws_host_url, - const std::optional& new_base_url) +void App::update_hostname(const std::string& host_url, const std::string& ws_host_url, + const std::string& new_base_url) { - // Update url components based on new hostname (and optional websocket hostname) values - log_debug("App: update_hostname: %1%2%3", host_url, ws_host_url ? util::format(" | %1", *ws_host_url) : "", - new_base_url ? util::format(" | base URL: %1", *new_base_url) : ""); - // Save the new base url, if provided - if (new_base_url) { - m_base_url = *new_base_url; - } + log_debug("App: update_hostname: %1 | %2 | %3", host_url, ws_host_url, new_base_url); + m_base_url = new_base_url; // If a new host url was returned from the server, use it to configure the routes // Otherwise, use the m_base_url value - configure_route(host_url.length() > 0 ? host_url : m_base_url, ws_host_url); + std::string base_url = host_url.length() > 0 ? host_url : m_base_url; + configure_route(base_url, ws_host_url); } // MARK: - Template specializations @@ -551,40 +491,29 @@ 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; - req.method = HttpMethod::post; - req.url = url_for_path(); - req.body = Bson(BsonDocument{{"name", name}}).to_string(); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request(std::move(req), user, - UserAPIKeyResponseHandler{std::move(completion)}); + m_auth_request_client.do_authenticated_request( + HttpMethod::post, url_for_path(), Bson(BsonDocument{{"name", name}}).to_string(), user, + RequestTokenType::RefreshToken, 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; - req.method = HttpMethod::get; - req.url = url_for_path(id.to_string()); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request(std::move(req), user, + m_auth_request_client.do_authenticated_request(HttpMethod::get, url_for_path(id.to_string()), "", user, + RequestTokenType::RefreshToken, UserAPIKeyResponseHandler{std::move(completion)}); } void App::UserAPIKeyProviderClient::fetch_api_keys( - const std::shared_ptr& user, + const std::shared_ptr& user, UniqueFunction&&, Optional)>&& completion) { - Request req; - req.method = HttpMethod::get; - req.url = url_for_path(); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request( - std::move(req), user, [completion = std::move(completion)](const Response& response) { + HttpMethod::get, url_for_path(), "", user, RequestTokenType::RefreshToken, + [completion = std::move(completion)](const Response& response) { if (auto error = AppUtils::check_for_errors(response)) { return completion({}, std::move(error)); } @@ -604,48 +533,96 @@ 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; - req.method = HttpMethod::del; - req.url = url_for_path(id.to_string()); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request(std::move(req), user, + m_auth_request_client.do_authenticated_request(HttpMethod::del, url_for_path(id.to_string()), "", user, + RequestTokenType::RefreshToken, 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; - req.method = HttpMethod::put; - req.url = url_for_path(util::format("%1/enable", id.to_string())); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request(std::move(req), user, - handle_default_response(std::move(completion))); + m_auth_request_client.do_authenticated_request( + HttpMethod::put, url_for_path(util::format("%1/enable", id.to_string())), "", user, + RequestTokenType::RefreshToken, 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; - req.method = HttpMethod::put; - req.url = url_for_path(util::format("%1/disable", id.to_string())); - req.uses_refresh_token = true; - m_auth_request_client.do_authenticated_request(std::move(req), user, - handle_default_response(std::move(completion))); + m_auth_request_client.do_authenticated_request( + HttpMethod::put, url_for_path(util::format("%1/disable", id.to_string())), "", user, + RequestTokenType::RefreshToken, handle_default_response(std::move(completion))); } // 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) { - return m_sync_manager->get_current_user(); + try { + return user.shared_from_this(); + } + catch (const std::bad_weak_ptr&) { + return nullptr; + } } -std::vector> App::all_users() const +std::shared_ptr App::get_user_for_id(const std::string& user_id) { - return m_sync_manager->all_users(); + 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) +{ + 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::shared_ptr App::get_existing_logged_in_user(std::string_view user_id) +{ + util::CheckedLockGuard lock(m_user_mutex); + if (auto it = m_users.find(std::string(user_id)); it != m_users.end() && it->second->is_logged_in()) { + 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 @@ -654,13 +631,11 @@ std::string App::get_base_url() const return m_base_url; } -void App::update_base_url(std::optional base_url, UniqueFunction)>&& completion) +void App::update_base_url(std::string_view new_base_url, UniqueFunction)>&& completion) { - std::string new_base_url = base_url.value_or(std::string(App::default_base_url())); - if (new_base_url.empty()) { // Treat an empty string the same as requesting the default base url - new_base_url = std::string(App::default_base_url()); + new_base_url = App::default_base_url(); log_debug("App::update_base_url: empty => %1", new_base_url); } else { @@ -668,9 +643,7 @@ void App::update_base_url(std::optional base_url, UniqueFunction base_url, UniqueFunction& sync_user, - UniqueFunction&, Optional)>&& completion) +std::vector> App::all_users() { - Request req; - req.method = HttpMethod::get; - req.url = url_for_path("/auth/profile"); - req.timeout_ms = m_request_timeout_ms; - req.uses_refresh_token = false; + 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) +{ do_authenticated_request( - std::move(req), sync_user, - [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) { + HttpMethod::get, url_for_path("/auth/profile"), "", user, RequestTokenType::AccessToken, + [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)); } @@ -710,53 +690,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); @@ -764,15 +744,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" : ""); @@ -781,124 +780,154 @@ void App::log_in_with_credentials( attach_auth_options(body); 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 { + make_request(HttpMethod::post, std::move(route), linking_user, RequestTokenType::AccessToken, + Bson(body).to_string()), + [completion = std::move(completion), credentials, linking_user, self = shared_from_this(), + this](auto&&, 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 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 + 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 (!user->is_logged_in()) { + return completion(nullptr, + AppError(ErrorCodes::BadToken, "Could not log in user: received malformed JWT")); + } + switch_user(user); + get_profile(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(); + auto request = + make_request(HttpMethod::del, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""); - 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)}); + 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) { + do_request(std::move(request), + [self = shared_from_this(), completion = std::move(completion)](auto&&, const Response& response) { auto error = AppUtils::check_for_errors(response); if (!error) { self->emit_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.")); @@ -907,28 +936,29 @@ 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)); - }); -} - -void App::link_user(const std::shared_ptr& user, const AppCredentials& credentials, - UniqueFunction&, Optional)>&& completion) + do_authenticated_request( + HttpMethod::del, url_for_path("/auth/delete"), "", user, RequestTokenType::AccessToken, + [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) { if (!user) { return completion(nullptr, @@ -938,20 +968,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)); @@ -968,9 +999,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, @@ -991,9 +1020,9 @@ void App::request_location(UniqueFunction)>&& compl // Release the lock before calling the completion function lock.unlock(); completion(util::none); - return; // early return + return; } - base_url = new_hostname ? *new_hostname : m_base_url; + base_url = new_hostname.value_or(m_base_url); // If this is for a redirect after querying new_hostname, then use the redirect location if (redir_location) app_route = get_app_route(redir_location); @@ -1009,22 +1038,21 @@ void App::request_location(UniqueFunction)>&& compl req.method = HttpMethod::get; req.url = util::format("%1/location", app_route); req.timeout_ms = m_request_timeout_ms; - req.redirect_count = redirect_count; log_debug("App: request location: %1", req.url); m_config.transport->send_request_to_server( - std::move(req), [self = shared_from_this(), completion = std::move(completion), - base_url = std::move(base_url)](Request&& request, const Response& response) mutable { + req, [self = shared_from_this(), completion = std::move(completion), base_url = std::move(base_url), + redirect_count](const Response& response) mutable { // Check to see if a redirect occurred if (AppUtils::is_redirect_status_code(response.http_status_code)) { // Make sure we don't do too many redirects (max_http_redirects (20) is an arbitrary number) - if (++request.redirect_count >= s_max_http_redirects) { + if (redirect_count >= s_max_http_redirects) { completion(AppError{ErrorCodes::ClientTooManyRedirects, util::format("number of redirections exceeded %1", s_max_http_redirects), {}, response.http_status_code}); - return; // early return + return; } // Handle the redirect response when requesting the location - extract the // new location header field and resend the request. @@ -1035,14 +1063,15 @@ void App::request_location(UniqueFunction)>&& compl "Redirect response missing location header", {}, response.http_status_code}); - return; // early return + return; } // try to request the location info at the new location in the redirect response // retry_count is passed in to track the number of subsequent redirection attempts self->request_location(std::move(completion), std::move(base_url), std::move(redir_location), - request.redirect_count); - return; // early return + redirect_count + 1); + return; } + // Location request was successful - update the location info auto update_response = self->update_location(response, base_url); if (update_response) { @@ -1062,7 +1091,6 @@ 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; } @@ -1072,19 +1100,17 @@ std::optional App::update_location(const Response& response, const std auto json = parse(response.body); auto hostname = get(json, "hostname"); auto ws_hostname = get(json, "ws_hostname"); - auto deployment_model = get(json, "deployment_model"); - auto location = get(json, "location"); - log_debug("App: Location info returned for deployment model: %1(%2)", deployment_model, location); - { - util::CheckedLockGuard guard(m_route_mutex); - // Update the local hostname and path information - update_hostname(hostname, ws_hostname, base_url); - m_location_updated = true; - if (m_sync_manager) { - // Provide the Device Sync websocket route to the SyncManager - m_sync_manager->set_sync_route(make_sync_route()); - } + std::optional sync_route; + read_field(json, "sync_route", sync_route); + + util::CheckedLockGuard guard(m_route_mutex); + // Update the local hostname and path information + update_hostname(hostname, ws_hostname, base_url); + m_location_updated = true; + if (!sync_route) { + sync_route = make_sync_route(); } + m_sync_manager->set_sync_route(*sync_route, true); } catch (const AppError& ex) { return ex; @@ -1092,7 +1118,7 @@ std::optional App::update_location(const Response& response, const std return util::none; } -void App::update_location_and_resend(Request&& request, UniqueFunction&& completion, +void App::update_location_and_resend(std::unique_ptr&& request, IntermediateCompletion&& completion, Optional&& redir_location) { // Update the location information if a redirect response was received or m_location_updated == false @@ -1102,23 +1128,21 @@ void App::update_location_and_resend(Request&& request, UniqueFunction error) mutable { if (error) { // Operation failed, pass it up the chain - return completion(AppUtils::make_apperror_response(*error)); + return completion(std::move(request), AppUtils::make_apperror_response(*error)); } // If the location info was updated, update the original request to point // to the new location URL. - auto comp = AppUtils::split_url(request.url); - if (!comp.is_ok()) { - throw Exception(comp.get_status()); - } - request.url = self->get_host_url() + comp.get_value().request; + auto url = util::Uri::parse(request->url); + request->url = + util::format("%1%2%3%4", self->get_host_url(), url.get_path(), url.get_query(), url.get_frag()); - self->log_debug("App: send_request(after location update): %1 %2", httpmethod_to_string(request.method), - request.url); + self->log_debug("App: send_request(after location update): %1 %2", request->method, request->url); // Retry the original request with the updated url + auto& request_ref = *request; self->m_config.transport->send_request_to_server( - std::move(request), [self = std::move(self), completion = std::move(completion)]( - Request&& request, const Response& response) mutable { + request_ref, [self = std::move(self), completion = std::move(completion), + request = std::move(request)](const Response& response) mutable { self->check_for_redirect_response(std::move(request), response, std::move(completion)); }); }, @@ -1128,21 +1152,17 @@ void App::update_location_and_resend(Request&& request, UniqueFunction)>&& completion, const BsonDocument& body) { - do_request(Request{HttpMethod::post, std::move(route), m_request_timeout_ms, get_request_headers(), - Bson(body).to_string()}, - handle_default_response(std::move(completion))); + do_request( + make_request(HttpMethod::post, std::move(route), nullptr, RequestTokenType::NoAuth, Bson(body).to_string()), + [completion = std::move(completion)](auto&&, const Response& response) { + completion(AppUtils::check_for_errors(response)); + }); } -void App::do_request(Request&& request, UniqueFunction&& completion, - bool update_location) +void App::do_request(std::unique_ptr&& request, IntermediateCompletion&& completion, bool update_location) { - // Make sure the timeout value is set to the configured request timeout value - request.timeout_ms = m_request_timeout_ms; - // Verify the request URL to make sure it is valid - if (auto comp = AppUtils::split_url(request.url); !comp.is_ok()) { - throw Exception(comp.get_status()); - } + util::Uri::parse(request->url); // Refresh the location info when app is created or when requested (e.g. after a websocket redirect) // to ensure the http and websocket URL information is up to date. @@ -1156,25 +1176,26 @@ void App::do_request(Request&& request, UniqueFunctionmethod, request->url); // If location info has already been updated, then send the request directly + auto& request_ref = *request; m_config.transport->send_request_to_server( - std::move(request), [self = shared_from_this(), completion = std::move(completion)]( - Request&& request, const Response& response) mutable { + request_ref, [self = shared_from_this(), completion = std::move(completion), + request = std::move(request)](const Response& response) mutable { self->check_for_redirect_response(std::move(request), response, std::move(completion)); }); } -void App::check_for_redirect_response(Request&& request, const Response& response, - UniqueFunction&& completion) +void App::check_for_redirect_response(std::unique_ptr&& request, const Response& response, + IntermediateCompletion&& completion) { // If this isn't a redirect response, then we're done if (!AppUtils::is_redirect_status_code(response.http_status_code)) { - return completion(response); + return completion(std::move(request), response); } // Handle a redirect response when sending the original request - extract the location @@ -1182,8 +1203,10 @@ void App::check_for_redirect_response(Request&& request, const Response& respons auto redir_location = AppUtils::extract_redir_location(response.headers); if (!redir_location) { // Location not found in the response, pass error response up the chain - return completion(AppUtils::make_clienterror_response( - ErrorCodes::ClientRedirectError, "Redirect response missing location header", response.http_status_code)); + return completion(std::move(request), + AppUtils::make_clienterror_response(ErrorCodes::ClientRedirectError, + "Redirect response missing location header", + response.http_status_code)); } // Request the location info at the new location - once this is complete, the original @@ -1191,90 +1214,95 @@ 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(HttpMethod method, std::string&& route, std::string&& body, + const std::shared_ptr& user, RequestTokenType token_type, util::UniqueFunction&& completion) { - request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken - : RequestTokenType::AccessToken); - - auto completion_2 = [completion = std::move(completion), request, sync_user, - self = shared_from_this()](const Response& response) mutable { + auto request = make_request(method, std::move(route), user, token_type, std::move(body)); + do_request(std::move(request), [token_type, user, completion = std::move(completion), self = shared_from_this()]( + std::unique_ptr&& request, const Response& response) mutable { if (auto error = AppUtils::check_for_errors(response)) { - self->handle_auth_failure(std::move(*error), std::move(response), std::move(request), sync_user, + self->handle_auth_failure(std::move(*error), std::move(request), response, user, token_type, std::move(completion)); } else { completion(response); } - }; - do_request(std::move(request), std::move(completion_2)); + }); } -void App::handle_auth_failure(const AppError& error, const Response& response, Request&& request, - const std::shared_ptr& sync_user, +void App::handle_auth_failure(const AppError& error, std::unique_ptr&& request, const Response& response, + const std::shared_ptr& user, RequestTokenType token_type, util::UniqueFunction&& completion) { // Only handle auth failures - if (*error.additional_status_code == 401) { - if (request.uses_refresh_token) { - if (sync_user && sync_user->is_logged_in()) { - sync_user->log_out(); - } - completion(response); - return; - } + if (*error.additional_status_code != 401) { + completion(response); + return; } - else { + + // If the refresh token is invalid then the user needs to be logged back + // in to be able to use it again + if (token_type == RequestTokenType::RefreshToken) { + if (user && user->is_logged_in()) { + user->log_out(); + } completion(response); 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)); - } - }); + // Otherwise we may be able to request a new access token and have the request succeed with that + refresh_access_token(user, false, + [self = shared_from_this(), request = std::move(request), completion = std::move(completion), + response = std::move(response), user](Optional&& error) mutable { + if (error) { + // pass the error back up the chain + completion(response); + return; + } + + // Reissue the request with the new access token + request->headers = get_request_headers(user, RequestTokenType::AccessToken); + self->do_request(std::move(request), + [completion = std::move(completion)](auto&&, auto& response) { + completion(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& user, bool update_location, util::UniqueFunction)>&& completion) { - if (!sync_user) { + if (!user) { completion(AppError(ErrorCodes::ClientUserNotFound, "No current user exists")); return; } - if (!sync_user->is_logged_in()) { + if (!user->is_logged_in()) { completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "The user is not logged in")); return; } - log_debug("App: refresh_access_token: email: %1 %2", sync_user->user_profile().email(), + log_debug("App: refresh_access_token: email: %1 %2", user->user_profile().email(), update_location ? "(updating location)" : ""); // If update_location is set, force the location info to be updated before sending the request 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) { + make_request(HttpMethod::post, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""), + [completion = std::move(completion), self = shared_from_this(), user](auto&&, 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(user->user_id())) { + data->access_token = access_token; + self->m_metadata_store->update_user(user->user_id(), *data); + user->update_backing_data(std::move(data)); + } } catch (AppError& err) { return completion(std::move(err)); @@ -1291,7 +1319,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) { @@ -1304,7 +1332,7 @@ void App::call_function(const std::shared_ptr& user, const std::string service_name_opt ? (",\"service\":" + nlohmann::json(service_name).dump()) : ""); do_authenticated_request( - Request{HttpMethod::post, function_call_url_path(), m_request_timeout_ms, {}, std::move(args), false}, user, + HttpMethod::post, function_call_url_path(), std::move(args), user, RequestTokenType::AccessToken, [self = shared_from_this(), name = name, service_name = std::move(service_name), completion = std::move(completion)](const Response& response) { if (auto error = AppUtils::check_for_errors(response)) { @@ -1316,7 +1344,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) { @@ -1358,7 +1386,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)); @@ -1368,16 +1396,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{ @@ -1406,10 +1434,56 @@ Request App::make_streaming_request(const std::shared_ptr& user, const }; } +std::unique_ptr App::make_request(HttpMethod method, std::string&& url, const std::shared_ptr& user, + RequestTokenType token_type, std::string&& body) const +{ + auto request = std::make_unique(); + request->method = method; + request->url = std::move(url); + request->body = std::move(body); + request->headers = get_request_headers(user, token_type); + request->timeout_ms = m_request_timeout_ms; + return request; +} + 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, 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 -} // namespace realm +} // namespace realm::app diff --git a/src/realm/object-store/sync/app.hpp b/src/realm/object-store/sync/app.hpp index 8c804c82e1e..9d23b5b05bb 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,70 +51,141 @@ 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); // Returns the default base_url for SDKs to use instead of defining their own static std::string_view default_base_url(); - // `enable_shared_from_this` is unsafe with public constructors; - // use `App::get_app()` instead - explicit App(Private, const Config& 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(); + + 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; - - /// Get all users. - std::vector> all_users() const; + const std::string& app_id() const noexcept + { + return m_config.app_id; + } - std::shared_ptr const& sync_manager() const + // MARK: - Other objects owned by App + const std::shared_ptr& sync_manager() const { return m_sync_manager; } + std::shared_ptr auth_request_client() + { + return std::shared_ptr(shared_from_this(), this); + } + + std::shared_ptr app_service_client() + { + return std::shared_ptr(shared_from_this(), this); + } + + // MARK: - User Management + + /// Get the last used user. + std::shared_ptr current_user() REQUIRES(!m_user_mutex); + /// Get the user object for the given `user_id` if a user with that id is logged in, or nullptr if not. + std::shared_ptr get_existing_logged_in_user(std::string_view user_id) REQUIRES(!m_user_mutex); + /// Get all 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 An `AppCredentials` 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. @@ -123,7 +193,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; @@ -140,41 +210,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; @@ -199,30 +269,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. @@ -231,7 +301,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. @@ -241,7 +311,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; @@ -253,21 +323,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. @@ -283,119 +350,46 @@ class App : public std::enable_shared_from_this, /// NOTE: If another App operation is started while this function is in progress, that request will use the /// original base URL location information. /// @param base_url The new base URL to use for future AppServices requests and sync websocket connections. If - /// not set or an empty string, the default Device Sync base_url will be used. + /// an empty string, the default Device Sync base_url will be used. /// @param completion A callback block to be invoked once the location update completes. - void update_base_url(std::optional base_url, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - /// Log in a user and asynchronously retrieve a user object. - /// If the log in completes successfully, the completion block will be called, and a - /// `SyncUser` representing the logged-in user will be passed to it. This user object - /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the - /// completion block will be called with an error. - /// - /// @param credentials A `SyncCredentials` object representing the user to log in. - /// @param completion A callback block to be invoked once the log in completes. - void log_in_with_credentials( - const AppCredentials& credentials, - util::UniqueFunction&, util::Optional)>&& completion) + void update_base_url(std::string_view base_url, util::UniqueFunction)>&& 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)); } @@ -406,8 +400,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)); @@ -415,33 +409,39 @@ 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); static std::string create_ws_host_url(const std::string_view host_url); + // 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 @@ -459,14 +459,16 @@ class App : public std::enable_shared_from_this, // (e.g. "https://us-east-1.aws.services.cloud.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; @@ -482,22 +484,24 @@ 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); + /// The completion type for all intermediate operations which occur before performing the original request + using IntermediateCompletion = util::UniqueFunction&&, const Response&)>; + /// Checks if an auth failure has taken place and if so it will attempt to refresh the /// access token and then perform the orginal request again with the new access token /// @param error The error to check for auth failures - /// @param response The original response to pass back should this not be an auth error /// @param request The request to perform /// @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, + void handle_auth_failure(const AppError& error, std::unique_ptr&& request, const Response& response, + const std::shared_ptr& user, RequestTokenType token_type, util::UniqueFunction&& completion) REQUIRES(!m_route_mutex); std::string url_for_path(const std::string& path) const override REQUIRES(!m_route_mutex); @@ -505,7 +509,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. @@ -513,7 +517,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); @@ -529,77 +533,89 @@ class App : public std::enable_shared_from_this, /// @param request The original request object that needs to be sent after the update /// @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); + void update_location_and_resend(std::unique_ptr&& request, IntermediateCompletion&& completion, + 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. /// @param request The request to be performed /// @param completion Returns the response from the server /// @param update_location Force the location metadata to be updated prior to sending the request - void do_request(Request&& request, util::UniqueFunction&& completion, + void do_request(std::unique_ptr&& request, IntermediateCompletion&& completion, bool update_location = false) REQUIRES(!m_route_mutex); + std::unique_ptr make_request(HttpMethod method, std::string&& url, const std::shared_ptr& user, + RequestTokenType, std::string&& body) const; + /// Process the redirect response received from the last request that was sent to the server /// @param request The request to be performed (in case it needs to be sent again) /// @param response The response from the send_request_to_server operation /// @param completion Returns the response from the server if not a redirect - void check_for_redirect_response(Request&& request, const Response& response, - util::UniqueFunction&& completion) - REQUIRES(!m_route_mutex); + void check_for_redirect_response(std::unique_ptr&& request, const Response& response, + IntermediateCompletion&& completion) REQUIRES(!m_route_mutex); - /// 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, - util::UniqueFunction&& completion) override - REQUIRES(!m_route_mutex); + void do_authenticated_request(HttpMethod, std::string&& route, std::string&& body, + const std::shared_ptr& user, RequestTokenType, + util::UniqueFunction&&) 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 credentials An `AppCredentials` object representing the user to log in. + /// @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); - // Requires locking m_route_mutex before calling - std::string make_sync_route(util::Optional ws_host_url = util::none) REQUIRES(m_route_mutex); + void configure_backing_store(std::unique_ptr store) REQUIRES(!m_route_mutex); - // Requires locking m_route_mutex before calling - void configure_route(const std::string& host_url, const std::optional& ws_host_url = std::nullopt) + 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); + void update_hostname(const std::string& host_url, const std::string& ws_host_url, const std::string& new_base_url) REQUIRES(m_route_mutex); - - // Requires locking m_route_mutex before calling - void update_hostname(const std::string& host_url, const std::optional& ws_host_url = std::nullopt, - const std::optional& new_base_url = std::nullopt) REQUIRES(m_route_mutex); std::string auth_route() REQUIRES(!m_route_mutex); std::string base_url() REQUIRES(!m_route_mutex); - bool verify_user_present(const std::shared_ptr& user) const; + bool verify_user_present(const std::shared_ptr& user) const REQUIRES(m_user_mutex); + + // UserProvider implementation + friend class User; + + util::CheckedMutex m_user_mutex; + mutable std::unordered_map m_users GUARDED_BY(m_user_mutex); + User* m_current_user GUARDED_BY(m_user_mutex) = nullptr; + + void register_sync_user(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..387b08cd3b5 --- /dev/null +++ b/src/realm/object-store/sync/app_config.hpp @@ -0,0 +1,113 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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; + // Used for requesting location metadata at startup and reconnecting sync connections. + // NOTE: delay_jitter_divisor is not configurable + sync::ResumptionDelayInfo reconnect_backoff_info; +}; + +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; + // If non-empty, mode is Encryption, and no key is explicitly set, the + // automatically generated key is stored in the keychain using this access + // group. Must be set when the metadata Realm is stored in an access group + // and shared between apps. Not applicable on non-Apple platforms. + std::string security_access_group; +}; + +} // 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..89a0331d595 --- /dev/null +++ b/src/realm/object-store/sync/app_user.cpp @@ -0,0 +1,313 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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->app_id()) + , m_user_id(user_id) +{ + REALM_ASSERT(m_app); + 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 +{ + // The server will sometimes send us an all-zero device ID as a way to + // explicitly signal that it did not generate a device ID for this login. + 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)); + return; + } + 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 9435a5959d2..f785645c8f0 100644 --- a/src/realm/object-store/sync/app_utils.cpp +++ b/src/realm/object-store/sync/app_utils.cpp @@ -16,17 +16,17 @@ // //////////////////////////////////////////////////////////////////////////// -#include "app_utils.hpp" #include + #include #include +#include #include #include -namespace realm { -namespace app { +namespace realm::app { const std::pair* AppUtils::find_header(const std::string& key_name, const std::map& search_map) @@ -45,34 +45,6 @@ AppUtils::find_header(const std::string& key_name, const std::map AppUtils::split_url(std::string url) -{ - UrlComponents comp; - // Find the position of the scheme separator "://" - size_t scheme_end_pos = url.find("://"); - if (scheme_end_pos == std::string::npos) { - // Missing scheme separator - return {ErrorCodes::BadServerUrl, util::format("URL missing scheme separator '://': %1", url)}; - } - comp.scheme = url.substr(0, scheme_end_pos); - url.erase(0, scheme_end_pos + std::char_traits::length("://")); - - // Find the first slash "/" - size_t host_end_pos = url.find("/"); - if (url.empty() || host_end_pos == 0) { - // No server provided - return {ErrorCodes::BadServerUrl, util::format("URL missing server: %1", url)}; - } - if (host_end_pos == std::string::npos) { - // No path/query/components section - comp.server = url; - return comp; - } - comp.server = url.substr(0, host_end_pos); - comp.request = url.substr(host_end_pos); - return comp; -} - bool AppUtils::is_success_status_code(int status_code) { return status_code == 0 || (status_code < 300 && status_code >= 200); @@ -93,7 +65,7 @@ std::optional AppUtils::extract_redir_location(const std::mapsecond.empty() && AppUtils::split_url(location->second).is_ok()) { + if (location && !location->second.empty() && util::Uri::try_parse(location->second).is_ok()) { // If the location is valid, return it wholesale (e.g., it could include a path for API proxies) return location->second; } @@ -107,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"); @@ -204,5 +176,4 @@ Response AppUtils::make_clienterror_response(ErrorCodes::Error code, const std:: return Response{http_status ? *http_status : 0, 0, {}, std::string(message), code}; } -} // namespace app -} // namespace realm +} // namespace realm::app diff --git a/src/realm/object-store/sync/app_utils.hpp b/src/realm/object-store/sync/app_utils.hpp index eb121c698df..2f6d1e4f666 100644 --- a/src/realm/object-store/sync/app_utils.hpp +++ b/src/realm/object-store/sync/app_utils.hpp @@ -31,15 +31,6 @@ struct Response; class AppUtils { public: - struct UrlComponents { - std::string scheme; // The scheme from the URL (e.g. https) - std::string server; // The complete server info ([userinfo@] hostname [:port]) - std::string request; // Everything after server info (path, query, parameters, etc.) - }; - // Split the URL into scheme, server and request parts - // returns nullopt if missing `://` or server info is empty - static StatusWith split_url(std::string url); - static std::optional check_for_errors(const Response& response); static Response make_apperror_response(const AppError& error); static Response make_clienterror_response(ErrorCodes::Error code, const std::string_view message, diff --git a/src/realm/object-store/sync/auth_request_client.hpp b/src/realm/object-store/sync/auth_request_client.hpp index 824246af9bd..58cbd905d75 100644 --- a/src/realm/object-store/sync/auth_request_client.hpp +++ b/src/realm/object-store/sync/auth_request_client.hpp @@ -16,17 +16,13 @@ // //////////////////////////////////////////////////////////////////////////// -#ifndef AUTH_REQUEST_CLIENT_HPP -#define AUTH_REQUEST_CLIENT_HPP +#ifndef REALM_OS_AUTH_REQUEST_CLIENT_HPP +#define REALM_OS_AUTH_REQUEST_CLIENT_HPP -#include -#include -#include +#include -namespace realm { -class SyncUser; -namespace app { -struct Request; +namespace realm::app { +class User; struct Response; class AuthRequestClient { @@ -35,11 +31,11 @@ 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(HttpMethod, std::string&& route, std::string&& body, + const std::shared_ptr& user, RequestTokenType, util::UniqueFunction&&) = 0; }; -} // namespace app -} // namespace realm +} // namespace realm::app -#endif /* AUTH_REQUEST_CLIENT_HPP */ +#endif /* REALM_OS_AUTH_REQUEST_CLIENT_HPP */ diff --git a/src/realm/object-store/sync/generic_network_transport.cpp b/src/realm/object-store/sync/generic_network_transport.cpp index dbfe545c07a..982fb5680e3 100644 --- a/src/realm/object-store/sync/generic_network_transport.cpp +++ b/src/realm/object-store/sync/generic_network_transport.cpp @@ -44,21 +44,21 @@ std::string http_message(const std::string& prefix, int code) } } // anonymous namespace -const char* httpmethod_to_string(HttpMethod method) +std::ostream& operator<<(std::ostream& os, HttpMethod method) { switch (method) { case HttpMethod::get: - return "GET"; + return os << "GET"; case HttpMethod::post: - return "POST"; + return os << "POST"; case HttpMethod::patch: - return "PATCH"; + return os << "PATCH"; case HttpMethod::put: - return "PUT"; + return os << "PUT"; case HttpMethod::del: - return "DEL"; + return os << "DEL"; } - return "UNKNOWN"; + return os << "UNKNOWN"; } AppError::AppError(ErrorCodes::Error ec, std::string message, std::string link, diff --git a/src/realm/object-store/sync/generic_network_transport.hpp b/src/realm/object-store/sync/generic_network_transport.hpp index 3d65cc0b9fb..f7d209c2fab 100644 --- a/src/realm/object-store/sync/generic_network_transport.hpp +++ b/src/realm/object-store/sync/generic_network_transport.hpp @@ -78,6 +78,7 @@ std::ostream& operator<<(std::ostream& os, AppError error); * An HTTP method type. */ enum class HttpMethod { get, post, patch, put, del }; +std::ostream& operator<<(std::ostream&, HttpMethod); /** * Request/Response headers type @@ -113,16 +114,11 @@ struct Request { * The body of the request. */ std::string body; - - /// Indicates if the request uses the refresh token or the access token - bool uses_refresh_token = false; - - /** - * A recursion counter to prevent too many redirects - */ - int redirect_count = 0; }; +/// What type of auth token should be used for a HTTP request. +enum class RequestTokenType { NoAuth, AccessToken, RefreshToken }; + /** * The contents of an HTTP response. */ @@ -158,21 +154,8 @@ struct GenericNetworkTransport { virtual ~GenericNetworkTransport() = default; virtual void send_request_to_server(const Request& request, util::UniqueFunction&& completion) = 0; - - void send_request_to_server(Request&& request, - util::UniqueFunction&& completion) - { - auto request_ptr = std::make_unique(std::move(request)); - const auto& request_ref = *request_ptr; - send_request_to_server(request_ref, [request_ptr = std::move(request_ptr), - completion = std::move(completion)](const Response& response) { - completion(std::move(*request_ptr), response); - }); - } }; -const char* httpmethod_to_string(HttpMethod method); - } // namespace realm::app #endif /* REALM_GENERIC_NETWORK_TRANSPORT_HPP */ 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..221c0304201 --- /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, const app::AppConfig& app_config) +{ + bool should_encrypt = app_config.metadata_mode == app::AppConfig::MetadataMode::Encryption; + if (!REALM_PLATFORM_APPLE && should_encrypt && !app_config.custom_encryption_key) + throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); + + if (app_config.custom_encryption_key && should_encrypt) + config.encryption_key = *app_config.custom_encryption_key; + if (app_config.custom_encryption_key || !should_encrypt || !REALM_PLATFORM_APPLE) { + config.clear_on_invalid_file = true; + return Realm::get_shared_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(app_config.app_id, app_config.security_access_group); + 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 recreate it + config.clear_on_invalid_file = true; + } + + // 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(app_config.app_id, app_config.security_access_group); + 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, const app::AppConfig& app_config, SyncFileManager& file_manager) + { + // Note that there are several deferred schema changes which don't + // justify bumping the schema version by themself, but should be done + // the next time something does justify a migration. + // These include: + // - remove FileActionSchema url and identity columns + // - rename current_user_identity to CurrentUserId + // - change most of the nullable columns to non-nullable + constexpr uint64_t SCHEMA_VERSION = 7; + + 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(); + 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, app_config); + 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, 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..37b44f45152 --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.hpp @@ -0,0 +1,91 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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; + // Get the user data for the given user if it exists and is not Removed, + // or nullopt otherwise. + 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 the given user's tokens and set its state to the given one (LoggedOut or Removed). + // If the user was the active user, a new active user is selected from the + // other logged in users, or set to null if there are none. If the new state + // is Removed, the user and their local Realm files are scheduled for deletion + // on next launch. + virtual void log_out(std::string_view user_id, SyncUser::State new_state) = 0; + // As log_out(user_id, State::Removed), but also attempt to immediately + // delete all of the user's local Realm files and only create file actions + // for ones which cannot be deleted immediately. + virtual void delete_user(SyncFileManager& file_manager, std::string_view user_id) = 0; + + // Get the user_id of the designated active user, or empty string if there + // are none. The active user will always be logged in, and there will always + // be an active user if any users are logged in. + virtual std::string get_current_user() = 0; + // Select the new active user. If the given user_id does not exist or is not + // a logged in user an arbitrary logged-in user will be used instead. + virtual void set_current_user(std::string_view user_id) = 0; + + // Get all non-Removed users, including ones which are currently logged out + 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 df368dd318f..7e07cd750fe 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, @@ -147,8 +146,6 @@ struct SyncClient { return m_client.notify_session_terminated(); } - ~SyncClient() {} - private: std::shared_ptr m_socket_provider; sync::Client m_client; @@ -159,7 +156,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..88def0eb62c 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,55 @@ 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); } +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 InvalidArgument(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 fcc0cd63785..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ /dev/null @@ -1,842 +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 -#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(const std::string& path, const SyncClientConfig& config, - std::string_view app_id) -{ - constexpr uint64_t SCHEMA_VERSION = 7; - - 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 (config.metadata_mode == SyncClientConfig::MetadataMode::Encryption && config.custom_encryption_key) - m_metadata_config.encryption_key = *config.custom_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(config, app_id); - - // 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(const SyncClientConfig& config, std::string_view app_id) -{ - bool should_encrypt = config.metadata_mode == SyncClientConfig::MetadataMode::Encryption; - if (!REALM_PLATFORM_APPLE && should_encrypt && !config.custom_encryption_key) - throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); - - if (config.custom_encryption_key || !should_encrypt || !REALM_PLATFORM_APPLE) { - m_metadata_config.clear_on_invalid_file = true; - 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(app_id, config.security_access_group); - 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 recreate it - m_metadata_config.clear_on_invalid_file = true; - } - - // 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(app_id, config.security_access_group); - 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 baf5d71a41a..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; -struct SyncClientConfig; - -// 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(const std::string& path, const SyncClientConfig& config, std::string_view app_id); - -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(const SyncClientConfig& config, std::string_view app_id); - - 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..3e2e52fd26b 100644 --- a/src/realm/object-store/sync/push_client.cpp +++ b/src/realm/object-store/sync/push_client.cpp @@ -27,36 +27,29 @@ namespace realm::app { PushClient::~PushClient() = default; -namespace { -util::UniqueFunction -wrap_completion(util::UniqueFunction)>&& completion) +void PushClient::request(const std::shared_ptr& user, HttpMethod method, std::string&& body, + util::UniqueFunction)>&& completion) { - return [completion = std::move(completion)](const Response& response) { - completion(AppUtils::check_for_errors(response)); - }; + auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); + std::string route = m_auth_request_client->url_for_path(push_route); + m_auth_request_client->do_authenticated_request(method, std::move(route), std::move(body), user, + RequestTokenType::AccessToken, + [completion = std::move(completion)](const Response& response) { + completion(AppUtils::check_for_errors(response)); + }); } -} // 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& user, util::UniqueFunction)>&& completion) { - auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); - std::string route = m_auth_request_client->url_for_path(push_route); - bson::BsonDocument args{{"registrationToken", registration_token}}; - m_auth_request_client->do_authenticated_request( - {HttpMethod::put, std::move(route), m_timeout_ms, {}, bson::Bson(args).to_string(), false}, sync_user, - wrap_completion(std::move(completion))); + request(user, HttpMethod::put, bson::Bson(args).to_string(), std::move(completion)); } -void PushClient::deregister_device(const std::shared_ptr& sync_user, +void PushClient::deregister_device(const std::shared_ptr& user, util::UniqueFunction)>&& completion) { - auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); - - m_auth_request_client->do_authenticated_request( - {HttpMethod::del, m_auth_request_client->url_for_path(push_route), m_timeout_ms, {}, "", false}, sync_user, - wrap_completion(std::move(completion))); + request(user, HttpMethod::del, "", std::move(completion)); } } // namespace realm::app diff --git a/src/realm/object-store/sync/push_client.hpp b/src/realm/object-store/sync/push_client.hpp index 2a904255172..b0a05038aca 100644 --- a/src/realm/object-store/sync/push_client.hpp +++ b/src/realm/object-store/sync/push_client.hpp @@ -16,28 +16,23 @@ // //////////////////////////////////////////////////////////////////////////// -#ifndef PUSH_CLIENT_HPP -#define PUSH_CLIENT_HPP +#ifndef REALM_OS_PUSH_CLIENT_HPP +#define REALM_OS_PUSH_CLIENT_HPP +#include #include -#include -#include -#include - -namespace realm { -class SyncUser; -namespace app { +namespace realm::app { class AuthRequestClient; +class User; struct AppError; class PushClient { public: - PushClient(const std::string& service_name, const std::string& app_id, uint64_t timeout_ms, + PushClient(const std::string& service_name, const std::string& app_id, std::shared_ptr&& auth_request_client) : m_service_name(service_name) , m_app_id(app_id) - , m_timeout_ms(timeout_ms) , m_auth_request_client(std::move(auth_request_client)) { } @@ -51,27 +46,28 @@ class PushClient { /// Register a device for push notifications. /// @param registration_token GCM registration token for the device. - /// @param sync_user The sync user requesting push registration. + /// @param 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, - util::UniqueFunction)>&& completion); + void register_device(const std::string& registration_token, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); /// Deregister a device for push notificatons, no token or device id needs to be passed /// as it is linked to the user in MongoDB Realm Cloud. - /// @param sync_user The sync user requesting push degistration. + /// @param 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, - util::UniqueFunction)>&& completion); + void deregister_device(const std::shared_ptr& user, + util::UniqueFunction)>&& completion); private: std::string m_service_name; std::string m_app_id; - uint64_t m_timeout_ms; std::shared_ptr m_auth_request_client; + + void request(const std::shared_ptr& user, HttpMethod method, std::string&& body, + util::UniqueFunction)>&& completion); }; -} // namespace app -} // namespace realm +} // namespace realm::app -#endif /* PUSH_CLIENT_HPP */ +#endif /* REALM_OS_PUSH_CLIENT_HPP */ 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 02e87babe36..0ba1d58bad5 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,66 +43,23 @@ SyncClientTimeouts::SyncClientTimeouts() { } -std::shared_ptr SyncManager::create(std::shared_ptr app, std::string 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), sync_route, config, app_id); + return std::make_shared(Private(), config); } -SyncManager::SyncManager(Private, std::shared_ptr app, std::string 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; - } - - m_metadata_manager = std::make_unique(m_file_manager->metadata_path(), m_config, app_id); - - 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. @@ -146,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(); } - - { - 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; - } } void SyncManager::set_log_level(util::Logger::Level level) noexcept @@ -189,6 +139,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 @@ -223,169 +174,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 @@ -401,13 +189,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. @@ -416,89 +197,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; } @@ -541,7 +259,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(); } @@ -550,15 +267,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() @@ -620,6 +329,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); @@ -643,12 +376,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 970fe7c8a56..f63c729bc5a 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,91 +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; - // Used for requesting location metadata at startup and reconnecting sync connections. - // NOTE: delay_jitter_divisor is not configurable - sync::ResumptionDelayInfo reconnect_backoff_info; -}; - -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; - std::optional> custom_encryption_key; - std::string security_access_group; - - 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. @@ -158,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); @@ -175,53 +100,11 @@ 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); @@ -229,28 +112,23 @@ class SyncManager : public std::enable_shared_from_this { // Force all the active sessions to restart void restart_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 a generated value based on the configured base_url when // the SyncManager is created by App. If this is incorrect, the websocket connection // will fail, resulting in an update to the access token (and the location, if it hasn't // been updated yet). - void set_sync_route(std::string sync_route, bool verified = true) REQUIRES(!m_mutex); - - std::pair sync_route() REQUIRES(!m_mutex) - { - util::CheckedLockGuard lock(m_mutex); - return std::make_pair(m_sync_route, m_sync_route_verified); - } + void set_sync_route(std::string sync_route, bool verified) REQUIRES(!m_mutex); - std::weak_ptr app() const REQUIRES(!m_mutex) + std::pair sync_route() 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, m_sync_route_verified}; } SyncClientConfig config() const REQUIRES(!m_mutex) @@ -268,10 +146,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::string sync_route, - const SyncClientConfig& config, const std::string& app_id); - SyncManager(Private, std::shared_ptr app, std::string 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; @@ -280,6 +160,22 @@ 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); + // The sync route URL for the sync connection to the server. + std::string m_sync_route GUARDED_BY(m_mutex); + // If true, then the sync route has been verified by querying the location info or successfully + // connecting to the server. + bool m_sync_route_verified GUARDED_BY(m_mutex) = false; + + // 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. @@ -290,53 +186,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 for the sync connection to the server. - std::string m_sync_route GUARDED_BY(m_mutex); - // If true, then the sync route has been verified by querying the location info or successfully - // connecting to the server. - bool m_sync_route_verified GUARDED_BY(m_mutex) = false; - - 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 9de4c49948a..a18c14cb90d 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 @@ -256,7 +256,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)) { @@ -302,10 +302,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 (ErrorCodes::error_categories(error->code()).test(ErrorCategory::client_error)) { + if (ErrorCodes::error_categories(error->code()).test(ErrorCategory::client_error)) { // any other client errors other than app_deallocated are considered fatal because // there was a problem locally before even sending the request to the server // eg. ClientErrorCode::user_not_found, ClientErrorCode::user_not_logged_in, @@ -400,13 +397,6 @@ SyncSession::SyncSession(Private, SyncClient& client, std::shared_ptr db, co } } -std::shared_ptr SyncSession::sync_manager() const -{ - util::CheckedLockGuard lk(m_state_mutex); - REALM_ASSERT(m_sync_manager); - return m_sync_manager->shared_from_this(); -} - void SyncSession::detach_from_sync_manager() { shutdown_and_wait(); @@ -418,21 +408,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) @@ -490,24 +474,25 @@ void SyncSession::download_fresh_realm(sync::ProtocolErrorInfo::Action server_re if (m_state != State::Active) { return; } - std::shared_ptr fresh_sync_session; + RealmConfig fresh_config; { util::CheckedLockGuard config_lock(m_config_mutex); - RealmConfig config = m_config; - config.path = fresh_path; + fresh_config = m_config; + fresh_config.path = fresh_path; // in case of migrations use the migrated config - auto fresh_config = m_migrated_sync_config ? *m_migrated_sync_config : *m_config.sync_config; + auto fresh_sync_config = m_migrated_sync_config ? *m_migrated_sync_config : *m_config.sync_config; // deep copy the sync config so we don't modify the live session's config - config.sync_config = std::make_shared(fresh_config); - config.sync_config->client_resync_mode = ClientResyncMode::Manual; - config.schema_version = m_previous_schema_version.value_or(m_config.schema_version); - fresh_sync_session = m_sync_manager->get_session(db, config); - auto& history = static_cast(*db->get_replication()); - // the fresh Realm may apply writes to this db after it has outlived its sync session - // the writes are used to generate a changeset for recovery, but are never committed - history.set_write_validator_factory({}); + fresh_config.sync_config = std::make_shared(fresh_sync_config); + fresh_config.sync_config->client_resync_mode = ClientResyncMode::Manual; + fresh_config.schema_version = m_previous_schema_version.value_or(m_config.schema_version); } + auto fresh_sync_session = m_sync_manager->get_session(db, fresh_config); + auto& history = static_cast(*db->get_replication()); + // the fresh Realm may apply writes to this db after it has outlived its sync session + // the writes are used to generate a changeset for recovery, but are never committed + history.set_write_validator_factory({}); + fresh_sync_session->assert_mutex_unlocked(); // The fresh realm uses flexible sync. if (auto fresh_sub_store = fresh_sync_session->get_flx_subscription_store()) { @@ -737,16 +722,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; @@ -802,7 +785,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)) { @@ -909,7 +892,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; @@ -1207,23 +1190,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)); } } @@ -1724,4 +1714,4 @@ void SyncSession::migrate_schema(util::UniqueFunction&& callback) session->wait_for_download_completion(std::move(callback)); session->resume(); }); -} \ No newline at end of file +} diff --git a/src/realm/object-store/sync/sync_session.hpp b/src/realm/object-store/sync/sync_session.hpp index 5728a1ee3cd..f96981ee364 100644 --- a/src/realm/object-store/sync/sync_session.hpp +++ b/src/realm/object-store/sync/sync_session.hpp @@ -96,9 +96,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; @@ -235,7 +233,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); @@ -387,12 +385,10 @@ 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); - 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 a94dfcab1b6..c80804ced20 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; @@ -1557,13 +1557,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()) @@ -2279,7 +2279,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 c9bffdbe8bd..b7be53e0bdb 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/network/default_socket.cpp b/src/realm/sync/network/default_socket.cpp index 30274accd64..9a5d16ab71c 100644 --- a/src/realm/sync/network/default_socket.cpp +++ b/src/realm/sync/network/default_socket.cpp @@ -90,8 +90,7 @@ class DefaultWebSocketImpl final : public DefaultWebSocket, public Config { constexpr bool was_clean = false; websocket_error_and_close_handler(was_clean, WebSocketError::websocket_write_error, ec.message()); } - void websocket_handshake_error_handler(std::error_code ec, const HTTPHeaders*, - const std::string_view* body) override + void websocket_handshake_error_handler(std::error_code ec, const HTTPHeaders*, std::string_view body) override { WebSocketError error = WebSocketError::websocket_ok; bool was_clean = true; @@ -121,11 +120,11 @@ class DefaultWebSocketImpl final : public DefaultWebSocket, public Config { else { error = WebSocketError::websocket_fatal_error; was_clean = false; - if (body) { + if (!body.empty()) { std::string_view identifier = "REALM_SYNC_PROTOCOL_MISMATCH"; - auto i = body->find(identifier); + auto i = body.find(identifier); if (i != std::string_view::npos) { - std::string_view rest = body->substr(i + identifier.size()); + std::string_view rest = body.substr(i + identifier.size()); // FIXME: Use std::string_view::begins_with() in C++20. auto begins_with = [](std::string_view string, std::string_view prefix) { return (string.size() >= prefix.size() && @@ -498,18 +497,13 @@ void DefaultWebSocketImpl::initiate_websocket_handshake() /// DefaultSocketProvider::DefaultSocketProvider(const std::shared_ptr& logger, - const std::string user_agent, + const std::string& user_agent, const std::shared_ptr& observer_ptr, AutoStart auto_start) : m_logger_ptr{std::make_shared(util::LogCategory::network, logger)} , m_observer_ptr{observer_ptr} - , m_service{} - , m_random{} , m_user_agent{user_agent} - , m_mutex{} , m_state{State::Stopped} - , m_state_cv{} - , m_thread{} { REALM_ASSERT(m_logger_ptr); // Make sure the logger is valid util::seed_prng_nondeterministically(m_random); // Throws diff --git a/src/realm/sync/network/default_socket.hpp b/src/realm/sync/network/default_socket.hpp index 0ef7556ac67..81c956631d1 100644 --- a/src/realm/sync/network/default_socket.hpp +++ b/src/realm/sync/network/default_socket.hpp @@ -50,13 +50,10 @@ class DefaultSocketProvider : public SyncSocketProvider { }; using AutoStart = util::TaggedBool; - DefaultSocketProvider(const std::shared_ptr& logger, const std::string user_agent, + DefaultSocketProvider(const std::shared_ptr& logger, const std::string& user_agent, const std::shared_ptr& observer_ptr = nullptr, AutoStart auto_start = AutoStart{true}); - // Don't allow move or copy constructor - DefaultSocketProvider(DefaultSocketProvider&&) = delete; - ~DefaultSocketProvider(); // Start the event loop if it is not started already. Otherwise, do nothing. diff --git a/src/realm/sync/network/websocket.cpp b/src/realm/sync/network/websocket.cpp index 85e0c5b007f..a173a72fd4c 100644 --- a/src/realm/sync/network/websocket.cpp +++ b/src/realm/sync/network/websocket.cpp @@ -588,6 +588,17 @@ class WebSocket { m_http_client.reset(new HTTPClient(m_config, m_logger_ptr)); m_frame_reader.reset(); + + if (m_test_handshake_response) { + HTTPResponse test_response; + test_response.status = HTTPStatus(*m_test_handshake_response); + test_response.body = std::move(m_test_handshake_response_body); + m_test_handshake_response.reset(); + m_test_handshake_response_body.clear(); + handle_http_response_received(std::move(test_response)); // Throws + return; + } + HTTPRequest req; req.method = HTTPMethod::Get; req.path = std::move(request_uri); @@ -754,7 +765,7 @@ class WebSocket { m_stopped = true; m_logger.error(util::LogCategory::network, "WebSocket: Received malformed HTTP response"); std::error_code ec = HttpError::bad_response_invalid_http; - m_config.websocket_handshake_error_handler(ec, nullptr, nullptr); // Throws + m_config.websocket_handshake_error_handler(ec, nullptr, {}); // Throws } void error_client_response_not_101(const HTTPResponse& response) @@ -769,9 +780,6 @@ class WebSocket { int status_code = int(response.status); std::error_code ec; - if (m_test_handshake_response) - status_code = *m_test_handshake_response; - if (status_code == 200) ec = HttpError::bad_response_200_ok; else if (status_code >= 200 && status_code < 300) @@ -806,16 +814,10 @@ class WebSocket { ec = HttpError::bad_response_unexpected_status_code; std::string_view body; - std::string_view* body_ptr = nullptr; - if (m_test_handshake_response) { - body = m_test_handshake_response_body; - body_ptr = &body; - } - else if (response.body) { + if (response.body) { body = *response.body; - body_ptr = &body; } - m_config.websocket_handshake_error_handler(ec, &response.headers, body_ptr); // Throws + m_config.websocket_handshake_error_handler(ec, &response.headers, body); // Throws } void error_client_response_websocket_headers_invalid(const HTTPResponse& response) @@ -828,12 +830,10 @@ class WebSocket { response); std::error_code ec = HttpError::bad_response_header_protocol_violation; std::string_view body; - std::string_view* body_ptr = nullptr; if (response.body) { body = *response.body; - body_ptr = &body; } - m_config.websocket_handshake_error_handler(ec, &response.headers, body_ptr); // Throws + m_config.websocket_handshake_error_handler(ec, &response.headers, body); // Throws } void error_server_malformed_request() @@ -841,7 +841,7 @@ class WebSocket { m_stopped = true; m_logger.error(util::LogCategory::network, "WebSocket: Received malformed HTTP request"); std::error_code ec = HttpError::bad_request_malformed_http; - m_config.websocket_handshake_error_handler(ec, nullptr, nullptr); // Throws + m_config.websocket_handshake_error_handler(ec, nullptr, {}); // Throws } void error_server_request_header_protocol_violation(std::error_code ec, const HTTPRequest& request) @@ -852,7 +852,7 @@ class WebSocket { "Websocket: HTTP request has invalid websocket headers." "HTTP request = \n%1", request); - m_config.websocket_handshake_error_handler(ec, &request.headers, nullptr); // Throws + m_config.websocket_handshake_error_handler(ec, &request.headers, {}); // Throws } void protocol_error(std::error_code ec) @@ -867,8 +867,7 @@ class WebSocket { m_logger.debug(util::LogCategory::network, "WebSocket::handle_http_response_received()"); m_logger.trace(util::LogCategory::network, "HTTP response = %1", response); - if (response.status != HTTPStatus::SwitchingProtocols || - (m_test_handshake_response && *m_test_handshake_response != 101)) { + if (response.status != HTTPStatus::SwitchingProtocols) { error_client_response_not_101(response); return; } diff --git a/src/realm/sync/network/websocket.hpp b/src/realm/sync/network/websocket.hpp index a38a7bc053f..c485ce9d384 100644 --- a/src/realm/sync/network/websocket.hpp +++ b/src/realm/sync/network/websocket.hpp @@ -62,8 +62,7 @@ class Config { /// It is safe to destroy the WebSocket object in these handlers. virtual void websocket_read_error_handler(std::error_code) = 0; virtual void websocket_write_error_handler(std::error_code) = 0; - virtual void websocket_handshake_error_handler(std::error_code, const HTTPHeaders*, - const std::string_view* body) = 0; + virtual void websocket_handshake_error_handler(std::error_code, const HTTPHeaders*, std::string_view body) = 0; virtual void websocket_protocol_error_handler(std::error_code) = 0; //@} diff --git a/src/realm/sync/noinst/server/server.cpp b/src/realm/sync/noinst/server/server.cpp index ca93cc6f859..e2a0d53e249 100644 --- a/src/realm/sync/noinst/server/server.cpp +++ b/src/realm/sync/noinst/server/server.cpp @@ -1217,8 +1217,7 @@ class SyncConnection : public websocket::Config { write_error(ec); } - void websocket_handshake_error_handler(std::error_code ec, const HTTPHeaders*, - const std::string_view*) final override + void websocket_handshake_error_handler(std::error_code ec, const HTTPHeaders*, std::string_view) final override { // WebSocket class has already logged a message for this error close_due_to_error(ec); // Throws 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/src/realm/util/uri.cpp b/src/realm/util/uri.cpp index e7a6bae1e05..81f444adc81 100644 --- a/src/realm/util/uri.cpp +++ b/src/realm/util/uri.cpp @@ -1,3 +1,9 @@ +#include + +#include +#include +#include + #include #include #include @@ -5,10 +11,6 @@ #include #include -#include -#include -#include - using namespace realm; @@ -21,7 +23,7 @@ using namespace realm; // reg-name = *( unreserved / pct-encoded / sub-delims ) -util::Uri::Uri(const std::string& str) +util::Uri::Uri(std::string_view str) { const char* b = str.data(); const char* e = b + str.size(); @@ -63,6 +65,26 @@ util::Uri::Uri(const std::string& str) m_frag.assign(b, e); // Throws } +StatusWith util::Uri::try_parse(std::string_view str) +{ + Uri uri(str); + if (uri.m_scheme.empty()) { + return {ErrorCodes::BadServerUrl, util::format("URL missing scheme: %1", str)}; + } + if (uri.m_auth.empty()) { + return {ErrorCodes::BadServerUrl, util::format("URL missing server: %1", str)}; + } + return uri; +} + +util::Uri util::Uri::parse(std::string_view str) +{ + auto status = try_parse(str); + if (!status.is_ok()) { + throw Exception(status.get_status()); + } + return status.get_value(); +} void util::Uri::set_scheme(const std::string& val) { diff --git a/src/realm/util/uri.hpp b/src/realm/util/uri.hpp index f8adda34047..164558ba226 100644 --- a/src/realm/util/uri.hpp +++ b/src/realm/util/uri.hpp @@ -1,10 +1,9 @@ #ifndef REALM_UTIL_URI_HPP #define REALM_UTIL_URI_HPP -#include +#include -namespace realm { -namespace util { +namespace realm::util { /// \brief A decomposed URI reference. @@ -74,7 +73,13 @@ class Uri { Uri(); /// Decompose the specified URI reference into its five main parts. - Uri(const std::string&); + Uri(std::string_view); + + /// Parse the given string, throwing if it's not a valid Uri + static Uri parse(std::string_view str); + + /// Parse the given string, returning an error if it's not a valid Uri. + static StatusWith try_parse(std::string_view str); /// Reconstruct a URI reference from its 5 components. std::string recompose() const; @@ -224,7 +229,6 @@ inline bool Uri::is_absolute() const return !m_scheme.empty(); } -} // namespace util -} // namespace realm +} // namespace realm::util #endif // REALM_UTIL_URI_HPP diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index 2be85d1eb8e..0926d77ed24 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 97886421c45..471684a8e22 100644 --- a/test/object-store/audit.cpp +++ b/test/object-store/audit.cpp @@ -167,7 +167,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"}; @@ -299,6 +299,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; @@ -1072,6 +1073,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); @@ -1501,6 +1503,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(); @@ -1535,7 +1538,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; @@ -1606,6 +1609,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); @@ -1632,6 +1636,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); @@ -1691,6 +1696,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; @@ -1749,6 +1755,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); @@ -1786,7 +1793,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"); @@ -1808,6 +1815,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 3fea181b669..b0bbc651877 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 d71df56d0be..7616e7a661f 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -538,17 +538,6 @@ TEST_CASE("C API (non-database)", "[c_api]") { #if REALM_ENABLE_SYNC SECTION("sync_client_config_t") { auto test_sync_client_config = cptr(realm_sync_client_config_new()); - realm_sync_client_config_set_base_file_path(test_sync_client_config.get(), "some string"); - CHECK(test_sync_client_config->base_file_path == "some string"); - realm_sync_client_config_set_metadata_mode(test_sync_client_config.get(), - RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED); - CHECK(test_sync_client_config->metadata_mode == - static_cast(RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED)); - auto enc_key = make_test_encryption_key(123); - realm_sync_client_config_set_metadata_encryption_key(test_sync_client_config.get(), - reinterpret_cast(enc_key.data())); - CHECK(test_sync_client_config->custom_encryption_key); - CHECK(std::equal(enc_key.begin(), enc_key.end(), test_sync_client_config->custom_encryption_key->begin())); realm_sync_client_config_set_reconnect_mode(test_sync_client_config.get(), RLM_SYNC_CLIENT_RECONNECT_MODE_TESTING); CHECK(test_sync_client_config->reconnect_mode == @@ -578,8 +567,6 @@ TEST_CASE("C API (non-database)", "[c_api]") { 600024); realm_sync_client_config_set_resumption_delay_backoff_multiplier(test_sync_client_config.get(), 1010); CHECK(test_sync_client_config->timeouts.reconnect_backoff_info.resumption_delay_backoff_multiplier == 1010); - realm_sync_client_config_set_security_access_group(test_sync_client_config.get(), "group.io.realm.test"); - CHECK(test_sync_client_config->security_access_group == "group.io.realm.test"); } SECTION("realm_app_config_t") { @@ -640,12 +627,30 @@ TEST_CASE("C API (non-database)", "[c_api]") { realm_app_config_set_bundle_id(app_config.get(), "some_bundle_id"); CHECK(app_config->device_info.bundle_id == "some_bundle_id"); + realm_app_config_set_base_file_path(app_config.get(), "some string"); + CHECK(app_config->base_file_path == "some string"); + + realm_app_config_set_metadata_mode(app_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); + CHECK(app_config->metadata_mode == app::AppConfig::MetadataMode::InMemory); + realm_app_config_set_metadata_mode(app_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED); + CHECK(app_config->metadata_mode == app::AppConfig::MetadataMode::Encryption); + realm_app_config_set_metadata_mode(app_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT); + CHECK(app_config->metadata_mode == app::AppConfig::MetadataMode::NoEncryption); + + realm_app_config_set_security_access_group(app_config.get(), "group.io.realm.test"); + CHECK(app_config->security_access_group == "group.io.realm.test"); + + auto enc_key = make_test_encryption_key(123); + realm_app_config_set_metadata_encryption_key(app_config.get(), reinterpret_cast(enc_key.data())); + CHECK(app_config->custom_encryption_key); + CHECK(std::equal(enc_key.begin(), enc_key.end(), app_config->custom_encryption_key->begin())); + 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); + realm_app_config_set_security_access_group(app_config.get(), ""); - 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) {}; @@ -675,7 +680,9 @@ TEST_CASE("C API (non-database)", "[c_api]") { realm_free(app_base_url); }; - auto update_and_check_base_url = [&](const char* new_base_url, const std::string_view expected) { + auto update_and_check_base_url = [&](const char* new_base_url, std::string_view expected) { + INFO(util::format("new_base_url: %1", new_base_url ? new_base_url : "")); + transport->set_base_url(expected); realm_app_update_base_url( test_app.get(), new_base_url, @@ -5633,7 +5640,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); @@ -5665,32 +5672,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; @@ -5706,8 +5709,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); @@ -6116,8 +6118,7 @@ TEST_CASE("C API app: link_user integration w/c_api transport", "[sync][app][c_a REQUIRE(request_context != nullptr); auto new_request = Request{HttpMethod(request.method), request.url, default_timeout_ms, std::move(headers), std::string(request.body, request.body_size)}; - user_data->logger->trace("CAPI: Request URL (%1): %2", httpmethod_to_string(new_request.method), - new_request.url); + user_data->logger->trace("CAPI: Request URL (%1): %2", new_request.method, new_request.url); user_data->logger->trace("CAPI: Request body: %1", new_request.body); user_data->transport->send_request_to_server(new_request, [&](const Response& response) mutable { std::vector c_headers; @@ -6214,8 +6215,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); @@ -6230,7 +6230,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 6711fc7ab68..cc549be7af9 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,11 @@ 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({}); + user->stored_completion = {}; - session->update_access_token(valid_token); util::EventLoop::main().run_until([&] { return called.load(); }); @@ -1163,25 +1177,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 +1202,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-metadata-v4.realm b/test/object-store/sync-metadata-v4.realm index fbaa1fbfac2..e5b6dbf7aad 100644 Binary files a/test/object-store/sync-metadata-v4.realm and b/test/object-store/sync-metadata-v4.realm differ diff --git a/test/object-store/sync-metadata-v5.realm b/test/object-store/sync-metadata-v5.realm index 76ecbf91c61..394f473ddf2 100644 Binary files a/test/object-store/sync-metadata-v5.realm and b/test/object-store/sync-metadata-v5.realm differ diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index de8b0ef1ac9..6326b95c105 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 @@ -59,15 +60,18 @@ using util::Optional; using namespace std::string_view_literals; using namespace std::literals::string_literals; +using namespace std::chrono_literals; +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); @@ -79,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; @@ -273,7 +277,7 @@ TEST_CASE("app: verify app error codes", "[sync][app][local]") { return false; } } - catch (const nlohmann::json::exception& ex) { + catch (const nlohmann::json::exception&) { // It's also a failure if parsing the json body throws an exception return false; } @@ -598,36 +602,6 @@ TEST_CASE("app: verify app error codes", "[sync][app][local]") { // MARK: - Verify generic app utils helper functions TEST_CASE("app: verify app utils helpers", "[sync][app][local]") { - SECTION("split_url") { - auto verify_good_url = [](std::string scheme, std::string server, std::string request) { - std::string url = util::format("%1://%2%3", scheme, server, request); - auto comp = AppUtils::split_url(url); - REQUIRE(comp.is_ok()); - REQUIRE(comp.get_value().scheme == scheme); - REQUIRE(comp.get_value().server == server); - REQUIRE(comp.get_value().request == request); - }; - - verify_good_url("https", "some.host.com", "/path/to/use?some_query=do-something#fragment"); - verify_good_url("wss", "localhost:9090", ""); - verify_good_url("scheme", "user:pass@host.com", "/"); - verify_good_url("mqtt", "host", "/some/path:that?is@not*really(valid)"); - - // Verify bad urls - auto comp = AppUtils::split_url("localhost/path"); - REQUIRE(!comp.is_ok()); - comp = AppUtils::split_url("http:localhost/path"); - REQUIRE(!comp.is_ok()); - comp = AppUtils::split_url("http:/localhost/path"); - REQUIRE(!comp.is_ok()); - comp = AppUtils::split_url("https://"); - REQUIRE(!comp.is_ok()); - comp = AppUtils::split_url("http:///localhost/path"); - REQUIRE(!comp.is_ok()); - comp = AppUtils::split_url(""); - REQUIRE(!comp.is_ok()); - } - SECTION("find_header") { std::map headers1 = {{"header1", "header1-value"}, {"HEADER2", "header2-value"}, @@ -787,7 +761,7 @@ TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][ CHECK(error->reason() == "name already in use"); CHECK(error->code() == ErrorCodes::AccountNameInUse); CHECK(!error->link_to_server_logs.empty()); - CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); + CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url)); processed = true; }); CHECK(processed); @@ -812,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); @@ -844,7 +818,7 @@ TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][ REQUIRE(error); CHECK(error->reason() == "invalid token data"); CHECK(!error->link_to_server_logs.empty()); - CHECK(error->link_to_server_logs.find(base_url) != std::string::npos); + CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url)); processed = true; }); CHECK(processed); @@ -949,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) { @@ -1009,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); @@ -1075,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; @@ -1235,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); @@ -1245,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); @@ -1253,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) { @@ -1332,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); @@ -2042,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; @@ -2124,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(); @@ -2204,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)); @@ -2247,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); @@ -2264,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)); @@ -2413,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); @@ -2473,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)); @@ -2516,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 { @@ -2611,10 +2587,9 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto app = session.app(); const auto partition = random_string(100); - // MARK: Add Objects - 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); @@ -2624,7 +2599,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); @@ -2635,7 +2610,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(); @@ -2649,7 +2624,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); @@ -2660,601 +2635,9 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } } - // MARK: Expired Session Refresh - - SECTION("Invalid Access Token is Refreshed") { - { - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - REQUIRE(get_dogs(r).size() == 0); - create_one_dog(r); - REQUIRE(get_dogs(r).size() == 1); - } - - { - create_user_and_log_in(app); - auto user = app->current_user(); - // set a bad access token. this will trigger a refresh when the sync session opens - user->update_access_token(encode_fake_jwt("fake_access_token")); - - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - Results dogs = get_dogs(r); - REQUIRE(dogs.size() == 1); - REQUIRE(dogs.get(0).get("breed") == "bulldog"); - REQUIRE(dogs.get(0).get("name") == "fido"); - } - } - { - auto redir_transport = std::make_shared(); - AutoVerifiedEmailCredentials creds; - - auto app_config = get_config(redir_transport, session.app_session()); - 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::NoEncryption; - - // initialize app and sync client - auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); - - SECTION("Test invalid redirect response") { - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - if (request_count == 0) { - logger->trace("request.url (%1): %2", request_count, request.url); - ++request_count; - return Response{301, 0, {{"Content-Type", "application/json"}}, "Some body data"}; - } - else if (request_count == 1) { - logger->trace("request.url (%1): %2", request_count, request.url); - return Response{ - 301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"}; - } - - return std::nullopt; - }; - - // This will fail due to no Location header - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientRedirectError); - REQUIRE(error->reason() == "Redirect response missing location header"); - }); - - // This will fail due to empty Location header - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientRedirectError); - REQUIRE(error->reason() == "Redirect response missing location header"); - }); - } - - SECTION("Test redirect response") { - int request_count = 0; - // redirect URL is localhost or 127.0.0.1 depending on what the initial value is - const std::string original_url = get_base_url(); - std::string original_host = original_url.substr(original_url.find("://") + 3); - original_host = original_host.substr(0, original_host.find("/")); - std::string original_ws_host = util::format("ws://%1", original_host); - std::string redirect_scheme = "http://"; - std::string websocket_scheme = "ws://"; - const std::string redirect_host = "fakerealm.example.com:9090"; - const std::string redirect_url = "http://fakerealm.example.com:9090"; - const std::string redirect_ws = "ws://fakerealm.example.com:9090"; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("Received request[%1]: %2", request_count, request.url); - if (request_count == 0) { - // First request should be to location - REQUIRE(request.url.find("/location") != std::string::npos); - if (request.url.find("https://") != std::string::npos) { - redirect_scheme = "https://"; - } - logger->trace("redirect_url (%1): %2", request_count, redirect_url); - request_count++; - } - else if (request_count == 1) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(!request.redirect_count); - ++request_count; - return Response{301, - 0, - {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request_count == 2) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find("somehost:9090") != std::string::npos); - ++request_count; - return Response{ - 308, 0, {{"Location", redirect_url}, {"Content-Type", "application/json"}}, "Some body data"}; - } - else if (request_count == 3) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find(redirect_url) != std::string::npos); - ++request_count; - return Response{ - 301, - 0, - {{"Location", redirect_scheme + original_host}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request_count == 4) { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); - // Let the init_app_metadata request go through - request_count++; - } - else if (request_count == 5) { - // This is the original request after the location has been updated - logger->trace("request.url (%1): %2", request_count, request.url); - // App metadata is no longer being used, query the host_url from app - REQUIRE(redir_app->get_host_url().find(original_host) != std::string::npos); - REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos); - // Validate the retry count tracked in the original message - request_count++; - } - return std::nullopt; - }; - - // This will be successful after a couple of retries due to the redirect response - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(!error); - }); - } - - SECTION("Test too many redirects") { - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request_count <= 21); - ++request_count; - return Response{request_count % 2 == 1 ? 308 : 301, - 0, - {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, - "Some body data"}; - }; - - redir_app->log_in_with_credentials( - realm::app::AppCredentials::username_password(creds.email, creds.password), - [&](std::shared_ptr user, util::Optional error) { - REQUIRE(!user); - REQUIRE(error); - REQUIRE(error->is_client_error()); - REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects); - REQUIRE(error->reason() == "number of redirections exceeded 20"); - }); - } - SECTION("Test server in maintenance") { - redir_transport->request_hook = [&](const Request&) -> std::optional { - nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"}, - {"error", "This service is currently undergoing maintenance"}, - {"link", "https://link.to/server_logs"}}; - return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()}; - }; - - redir_app->log_in_with_credentials( - realm::app::AppCredentials::username_password(creds.email, creds.password), - [&](std::shared_ptr user, util::Optional error) { - REQUIRE(!user); - REQUIRE(error); - REQUIRE(error->is_service_error()); - REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress); - REQUIRE(error->reason() == "This service is currently undergoing maintenance"); - REQUIRE(error->link_to_server_logs == "https://link.to/server_logs"); - REQUIRE(*error->additional_status_code == 500); - }); - } - } - SECTION("Test app redirect with no metadata") { - auto redir_transport = std::make_shared(); - AutoVerifiedEmailCredentials creds, creds2; - - auto app_config = get_config(redir_transport, session.app_session()); - 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; - - // initialize app and sync client - auto redir_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); - - int request_count = 0; - const std::string original_url = get_base_url(); - std::string original_host = original_url.substr(original_url.find("://") + 3); - original_host = original_host.substr(0, original_host.find("/")); - std::string original_ws_host = util::format("ws://%1", original_host); - const std::string redirect_url = "http://fakerealm.example.com:9090"; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be to location - REQUIRE(request.url.find("/location") != std::string::npos); - logger->trace("original_url (%1): %2", request_count, original_url); - } - else if (request_count++ == 1) { - REQUIRE(!request.redirect_count); - return Response{ - 308, 0, {{"Location", redirect_url}, {"Content-Type", "application/json"}}, "Some body data"}; - } - else if (request_count++ == 2) { - REQUIRE(request.url.find("location") != std::string::npos); - // app hostname will be updated via the metadata info - return Response{ - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%1\",\"ws_" - "hostname\":\"%2\"}", - original_url, original_ws_host)}; - } - else { - REQUIRE(request.url.find(original_url) != std::string::npos); - } - return std::nullopt; - }; - - // This will be successful after a couple of retries due to the redirect response - redir_app->provider_client().register_email( - creds.email, creds.password, [&](util::Optional error) { - REQUIRE(!error); - }); - auto [sync_route, verified] = app->sync_manager()->sync_route(); - REQUIRE(sync_route.find(original_ws_host) != std::string::npos); - REQUIRE(verified); - - // Register another email address and verify location data isn't requested again - request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - REQUIRE(request.url.find("location") == std::string::npos); - request_count++; - return std::nullopt; - }; - - redir_app->provider_client().register_email( - creds2.email, creds2.password, [&](util::Optional error) { - REQUIRE(!error); - }); - } - - SECTION("Test websocket redirect with existing session") { - std::string configured_app_url = get_base_url(); - std::string original_host = configured_app_url.substr(configured_app_url.find("://") + 3); - original_host = original_host.substr(0, original_host.find("/")); - std::string original_address = original_host; - uint16_t original_port = 443; - if (auto port_pos = original_host.find(":"); port_pos != std::string::npos) { - auto original_port_str = original_host.substr(port_pos + 1); - - original_port = strtol(original_port_str.c_str(), nullptr, 10); - original_address = original_host.substr(0, port_pos); - } - - std::string redirect_scheme = "http://"; - std::string websocket_scheme = "ws://"; - const std::string redirect_address = "fakerealm.example.com"; - const std::string redirect_host = "fakerealm.example.com:9090"; - const std::string redirect_url = "http://fakerealm.example.com:9090"; - - auto redir_transport = std::make_shared(); - auto redir_provider = std::make_shared(logger, ""); - redir_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint&& ep) { - ep.address = original_address; - ep.port = original_port; - return ep; - }; - std::mutex logout_mutex; - std::condition_variable logout_cv; - bool logged_out = false; - - auto server_app_config = minimal_app_config("websocket_redirect", schema); - TestAppSession test_session(create_app(server_app_config), redir_transport, DeleteApp{true}, - realm::ReconnectMode::normal, redir_provider); - auto partition = random_string(100); - auto user1 = test_session.app()->current_user(); - SyncTestFile r_config(user1, partition, schema); - // Override the default - r_config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { - if (error.status == ErrorCodes::AuthError) { - util::format(std::cerr, "Websocket redirect test: User logged out\n"); - std::unique_lock lk(logout_mutex); - logged_out = true; - logout_cv.notify_one(); - return; - } - util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n", - error.status); - abort(); - }; - - auto r = Realm::get_shared_realm(r_config); - - REQUIRE(!wait_for_download(*r)); - - SECTION("Valid websocket redirect") { - auto sync_manager = test_session.sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&logger, - &connect_count]() -> std::optional { - logger->trace("websocket connect (%1)", ++connect_count); - if (connect_count == 1) - return SocketProviderError(sync::HTTPStatus::PermanentRedirect); - if (connect_count == 2) - return SocketProviderError(sync::websocket::WebSocketError::websocket_moved_permanently); - return std::nullopt; - }; - redir_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint&& ep) { - if (connect_count < 2) { - return ep; - } - REQUIRE(ep.address == redirect_address); - ep.address = original_address; - ep.port = original_port; - return ep; - }; - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - return Response{static_cast(sync::HTTPStatus::PermanentRedirect), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request.url.find("/location") != std::string::npos) { - REQUIRE(request.url.find(redirect_host) != std::string::npos); - ++request_count; - return Response{ - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format( - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" - "hostname\":\"%3%1\"}", - redirect_host, redirect_scheme, websocket_scheme)}; - } - else if (request.url.find(redirect_host) != std::string::npos) { - auto new_req = request; - new_req.url = util::format("%1%2", configured_app_url, request.url.substr(redirect_url.size())); - logger->trace("Proxying request from %1 to %2", request.url, new_req.url); - auto resp = do_http_request(new_req); - logger->trace("Response: \"%1\"", resp.body); - return resp; - } - return std::nullopt; - }; - sync_session->resume(); - REQUIRE(!wait_for_download(*r)); - REQUIRE(user1->is_logged_in()); - - // Verify session is using the updated server url from the redirect - auto server_url = sync_session->full_realm_url(); - auto verified = sync_session->realm_url_verified(); - logger->trace("FULL_REALM_URL: %1 (%2)", server_url, verified ? "verified" : "not verified"); - REQUIRE((server_url.find(redirect_host) != std::string::npos)); - REQUIRE(verified); - } - SECTION("Websocket redirect logs out user") { - auto sync_manager = test_session.sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&connect_count]() -> std::optional { - if (connect_count++ > 0) - return std::nullopt; - - return SocketProviderError(sync::HTTPStatus::MovedPermanently); - }; - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - return Response{static_cast(sync::HTTPStatus::MovedPermanently), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else if (request.url.find("/location") != std::string::npos) { - return Response{ - static_cast(sync::HTTPStatus::Ok), - 0, - {{"Content-Type", "application/json"}}, - util::format( - "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_" - "hostname\":\"%3%1\"}", - redirect_host, redirect_scheme, websocket_scheme)}; - } - else if (request.url.find("auth/session") != std::string::npos) { - return Response{static_cast(sync::HTTPStatus::Unauthorized), - 0, - {{"Content-Type", "application/json"}}, - ""}; - } - return std::nullopt; - }; - - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - sync_session->resume(); - REQUIRE(wait_for_download(*r)); - std::unique_lock lk(logout_mutex); - auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { - return logged_out; - }); - REQUIRE(result); - REQUIRE(!user1->is_logged_in()); - } - SECTION("Too many websocket redirects logs out user") { - auto sync_manager = test_session.sync_manager(); - auto sync_session = sync_manager->get_existing_session(r->config().path); - sync_session->pause(); - - int connect_count = 0; - redir_provider->websocket_connect_func = [&connect_count]() -> std::optional { - if (connect_count++ > 0) - return std::nullopt; - - return SocketProviderError(sync::HTTPStatus::MovedPermanently); - }; - int request_count = 0; - const int max_http_redirects = 20; // from app.cpp in object-store - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(original_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - REQUIRE(request.redirect_count == 0); - } - if (request.url.find("/location") != std::string::npos) { - // Keep returning the redirected response - REQUIRE(request.redirect_count < max_http_redirects); - return Response{static_cast(sync::HTTPStatus::MovedPermanently), - 0, - {{"Location", redirect_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - else { - FAIL("should not get any other types of requests during the test - the log out is local"); - } - return std::nullopt; - }; - - SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager); - sync_session->resume(); - REQUIRE(wait_for_download(*r)); - std::unique_lock lk(logout_mutex); - auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { - return logged_out; - }); - REQUIRE(result); - REQUIRE(!user1->is_logged_in()); - } - } - - SECTION("Test websocket location update with invalid ws host url") { - std::string configured_app_url = get_base_url(); - std::string original_host = configured_app_url.substr(configured_app_url.find("://") + 3); - original_host = original_host.substr(0, original_host.find("/")); - std::string original_address = original_host; - uint16_t original_port = 443; - if (auto port_pos = original_host.find(":"); port_pos != std::string::npos) { - auto original_port_str = original_host.substr(port_pos + 1); - - original_port = strtol(original_port_str.c_str(), nullptr, 10); - original_address = original_host.substr(0, port_pos); - } - - auto redir_transport = std::make_shared(); - auto redir_provider = std::make_shared(logger, ""); - redir_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint&& ep) { - ep.address = original_address; - ep.port = original_port; - return ep; - }; - - // Create App and User and log in - auto server_app_config = minimal_app_config("websocket_location_update", schema); - TestAppSession test_session(create_app(server_app_config), redir_transport, DeleteApp{true}, - realm::ReconnectMode::normal, redir_provider); - auto partition = random_string(100); - - { - // Open the realm with the current user - auto user = test_session.app()->current_user(); - REQUIRE(user); - SyncTestFile r_config(user, partition, schema); - auto r = Realm::get_shared_realm(r_config); - REQUIRE(!wait_for_download(*r)); - } - - // Close the app - test_session.close(); - - // Set the base URL to an invalid value - std::string fake_host = "http://fakerealm.example.com:1234"; - test_session.app_config.base_url = fake_host; - test_session.sc_config.multiplex_sessions = GENERATE(true, false); - - // Set up the socket provider and transport to report the correct server - int connect_count = 0; - redir_provider->websocket_endpoint_resolver = [&connect_count](sync::WebSocketEndpoint&& ep) { - // Verify the first websocket attempt uses the iniital fake URL - ++connect_count; - if (connect_count == 1) { - REQUIRE(ep.address == "fakerealm.example.com"); - REQUIRE(ep.port == 1234); - } - return ep; - }; - - redir_provider->websocket_connect_func = [&connect_count]() -> std::optional { - if (connect_count == 1) { - return SocketProviderError(sync::websocket::WebSocketError::websocket_fatal_error); - } - return std::nullopt; - }; - - int request_count = 0; - redir_transport->request_hook = [&](const Request& request) -> std::optional { - logger->trace("request.url (%1): %2", request_count, request.url); - if (request_count++ == 0) { - // First request should be a location request against the original URL - REQUIRE(request.url.find(fake_host) != std::string::npos); - REQUIRE(request.url.find("/location") != std::string::npos); - return Response{static_cast(sync::HTTPStatus::PermanentRedirect), - 0, - {{"Location", configured_app_url}, {"Content-Type", "application/json"}}, - "Some body data"}; - } - return std::nullopt; - }; - - // Reopen the app, but don't log in at this point - test_session.reopen(false); - - // Open the realm with the cached user - int num_realms = GENERATE(1, 3); - auto user = test_session.app()->current_user(); - REQUIRE(user); - // Verify no transport calls have been made at this point since the - // app was re-opened. - REQUIRE(request_count == 0); - SyncTestFile r_config(user, partition, schema); - - std::vector realms; - for (int i = 0; i < num_realms; i++) { - SyncTestFile r_config(user, partition, schema); - realms.push_back(Realm::get_shared_realm(r_config)); - } - // wait for download - for (auto& realm : realms) { - REQUIRE(!wait_for_download(*realm)); - } - } - 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); @@ -3262,13 +2645,13 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { REQUIRE(get_dogs(r).size() == 1); } - auto transport = std::make_shared(); + 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()); @@ -3279,7 +2662,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) { @@ -3289,7 +2672,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); @@ -3301,8 +2684,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); @@ -3316,19 +2699,20 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { REQUIRE(token.expires); REQUIRE(token.timestamp < token.expires); std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - using namespace std::chrono_literals; token.expires = std::chrono::system_clock::to_time_t(now - 30s); REQUIRE(token.expired(now)); } - auto transport = std::make_shared(); + 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") { @@ -3338,7 +2722,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; @@ -3346,7 +2730,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); @@ -3357,17 +2741,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); @@ -3389,8 +2782,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); } @@ -3401,15 +2804,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 { @@ -3418,10 +2820,15 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } return std::nullopt; }; - SyncTestFile config(app, partition, schema); - auto r = Realm::get_shared_realm(config); - create_one_dog(r); - timed_wait_for( + 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( [&] { return did_receive_valid_token.load(); }, @@ -3460,8 +2867,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)); @@ -3477,7 +2883,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(); @@ -3490,7 +2898,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()); { @@ -3520,11 +2928,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); @@ -3533,7 +2941,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); @@ -3551,10 +2959,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)); @@ -3585,9 +2993,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)); @@ -3604,28 +3012,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); @@ -3635,14 +3045,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; @@ -3653,7 +3063,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; @@ -3691,7 +3101,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 = @@ -3730,7 +3140,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); @@ -3757,7 +3167,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; { @@ -3798,7 +3208,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"; @@ -3810,7 +3220,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(); }); @@ -3844,7 +3254,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); @@ -3862,9 +3272,351 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } } -TEST_CASE("app: base_url", "[sync][app][base_url]") { +TEST_CASE("app: redirect handling", "[sync][pbs][app]") { + auto logger = util::Logger::get_default_logger(); + + const auto schema = get_default_schema(); + + auto transport = std::make_shared>(); + auto socket_provider = std::make_shared(logger, ""); + OfflineAppSession::Config oas_config(transport); + oas_config.base_url = "http://original.invalid:9090"; + oas_config.socket_provider = socket_provider; + OfflineAppSession oas(oas_config); + AutoVerifiedEmailCredentials creds; + auto app = oas.app(); + const auto partition = random_string(100); + + SECTION("invalid redirect response reports and error") { + int request_count = 0; + + // This will fail due to no Location header + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request_count++ == 0); + return Response{301, 0, {{"Content-Type", "application/json"}}, "Some body data"}; + }; + app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientRedirectError); + REQUIRE(error->reason() == "Redirect response missing location header"); + }); + + // This will fail due to empty Location header + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request_count++ == 1); + return Response{301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"}; + }; + + app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientRedirectError); + REQUIRE(error->reason() == "Redirect response missing location header"); + }); + } + + SECTION("valid redirect response") { + int request_count = 0; + const std::string second_host = "http://second.invalid:9091"; + const std::string third_host = "http://third.invalid:9092"; + + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("Received request[%1]: %2", request_count, request.url); + switch (request_count++) { + case 0: + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring(*oas_config.base_url)); + return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""}; + + case 1: + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring(second_host)); + return Response{301, 0, {{"Location", third_host}, {"Content-Type", "application/json"}}, ""}; + + case 2: + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring(third_host)); + return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""}; + + case 3: + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring(second_host)); + return std::nullopt; + + default: + // some.fake.url is the location reported by UnitTestTransport + REQUIRE_THAT(request.url, ContainsSubstring("https://some.fake.url")); + return std::nullopt; + } + }; + + // This will be successful after a couple of retries due to the redirect response + app->provider_client().register_email( + creds.email, creds.password, [&](util::Optional error) { + REQUIRE(!error); + }); + } + + SECTION("too many redirects eventually reports an error") { + int request_count = 0; + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + REQUIRE(request_count < 21); + ++request_count; + return Response{request_count % 2 == 1 ? 308 : 301, + 0, + {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}}, + "Some body data"}; + }; + + app->log_in_with_credentials(app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, util::Optional error) { + REQUIRE(!user); + REQUIRE(error); + REQUIRE(error->is_client_error()); + REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects); + REQUIRE(error->reason() == "number of redirections exceeded 20"); + }); + REQUIRE(request_count == 21); + } + + SECTION("server in maintenance reports error") { + transport->request_hook = [&](const Request&) -> std::optional { + nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"}, + {"error", "This service is currently undergoing maintenance"}, + {"link", "https://link.to/server_logs"}}; + return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()}; + }; + + app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password), + [&](std::shared_ptr user, util::Optional error) { + REQUIRE(!user); + REQUIRE(error); + REQUIRE(error->is_service_error()); + REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress); + REQUIRE(error->reason() == + "This service is currently undergoing maintenance"); + REQUIRE(error->link_to_server_logs == "https://link.to/server_logs"); + REQUIRE(*error->additional_status_code == 500); + }); + } + + SECTION("websocket redirects update existing session") { + SyncServer server({}); + + transport->request_hook = [&](const Request& req) -> std::optional { + if (req.url.find("/location") != std::string::npos) { + return Response{ + 200, + 0, + {}, + nlohmann::json({ + {"hostname", "http://some.fake.url"}, + {"ws_hostname", "ws://ws.some.fake.url"}, + {"sync_route", "ws://some.fake.url/realm-sync"}, + }) + .dump(), + }; + } + return std::nullopt; + }; + + // The location info is fake, so we need to override it with the actual + // server endpoint + socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) { + ep.address = "127.0.0.1"; + ep.port = server.port(); + }; + + SyncTestFile realm_config(oas, "test"); + + std::mutex logout_mutex; + std::condition_variable logout_cv; + bool logged_out = false; + realm_config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + if (error.status == ErrorCodes::AuthError) { + { + std::unique_lock lk(logout_mutex); + logged_out = true; + } + logout_cv.notify_one(); + return; + } + util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n", + error.status); + abort(); + }; + + auto r = Realm::get_shared_realm(realm_config); + REQUIRE(!wait_for_download(*r)); + auto sync_session = r->sync_session(); + sync_session->pause(); + SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*oas.sync_manager()); + + int connect_count = 0; + socket_provider->websocket_connect_func = [&]() -> std::optional { + // Report a 308 response the first time we try to reconnect the websocket, + // which should result in App performing a location update. + // The actual Location header isn't used when we get a redirect on + // the websocket, so we don't need to supply it here + if (connect_count++ > 0) + return std::nullopt; + return sync::HTTPStatus::PermanentRedirect; + }; + + SECTION("valid websocket redirect") { + socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) { + logger->trace("resolve attempt %1: %2", connect_count, ep.address); + // First call happens after the call to the above hook which will + // force a 308 response. Second call happens after the redirect + // has been handled. + REQUIRE(connect_count <= 2); + if (connect_count == 2) { + REQUIRE(ep.address == "ws.invalid"); + } + + // Overriding the handshake result happens after dns resolution, + // so we need to set it to a valid endpoint for even the first call + ep.address = "127.0.0.1"; + ep.port = server.port(); + }; + + int request_count = 0; + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + ++request_count; + + // First request should be a location request against the original URL + if (request_count == 1) { + REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url")); + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + return Response{static_cast(sync::HTTPStatus::PermanentRedirect), + 0, + {{"Location", "http://asdf.invalid"}}, + ""}; + } + + // Second request should be a location request against the new URL + if (request_count == 2) { + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid")); + return Response{200, + 0, + {}, + nlohmann::json({ + {"hostname", "http://http.invalid"}, + {"ws_hostname", "ws://ws.invalid"}, + {"sync_route", "ws://ws.invalid/realm-sync"}, + }) + .dump()}; + } + + // Rest of the requests get handled normally + return std::nullopt; + }; + + sync_session->resume(); + REQUIRE(!wait_for_download(*r)); + REQUIRE(request_count > 1); + REQUIRE(realm_config.sync_config->user->is_logged_in()); + + // Verify session is using the updated server url from the redirect + auto server_url = sync_session->full_realm_url(); + REQUIRE_THAT(server_url, ContainsSubstring("ws.invalid")); + } + + SECTION("websocket redirect into auth error logs out user") { + int request_count = 0; + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + ++request_count; + + if (request_count == 1) { + // First request should be a location request against the original URL + REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url")); + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + return Response{static_cast(sync::HTTPStatus::PermanentRedirect), + 0, + {{"Location", "http://asdf.invalid"}}, + ""}; + } + + // Second request should be a location request against the new URL + if (request_count == 2) { + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid")); + return Response{200, + 0, + {}, + nlohmann::json({ + {"hostname", "http://http.invalid"}, + {"ws_hostname", "ws://ws.invalid"}, + }) + .dump()}; + } + + // Third request should be for an acccess token, which we reject + REQUIRE(request_count == 3); + REQUIRE_THAT(request.url, ContainsSubstring("auth/session")); + return Response{static_cast(sync::HTTPStatus::Unauthorized), 0, {}, ""}; + }; + + sync_session->resume(); + REQUIRE(wait_for_download(*r)); + std::unique_lock lk(logout_mutex); + auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { + return logged_out; + }); + REQUIRE(result); + REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in()); + } + + SECTION("too many websocket redirects logs out user") { + int request_count = 0; + const int max_http_redirects = 20; // from app.cpp in object-store + transport->request_hook = [&](const Request& request) -> std::optional { + logger->trace("request.url (%1): %2", request_count, request.url); + + // The test should never request anything other than /location + // even though the user is set to the logged-out state as trying + // to log out on the server needs to go through /location first too + REQUIRE_THAT(request.url, ContainsSubstring("/location")); + REQUIRE(request_count <= max_http_redirects); + + // First request should be a location request against the original URL + // and rest should use the redirect url + if (request_count++ == 0) { + REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url")); + } + else { + REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid")); + } + // Keep returning the redirected response + return Response{static_cast(sync::HTTPStatus::MovedPermanently), + 0, + {{"Location", "http://asdf.invalid"}}, + ""}; + }; + + sync_session->resume(); + REQUIRE(wait_for_download(*r)); + std::unique_lock lk(logout_mutex); + auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() { + return logged_out; + }); + REQUIRE(result); + REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in()); + } + } +} - struct BaseUrlTransport : GenericNetworkTransport { +TEST_CASE("app: base_url", "[sync][app][base_url]") { + struct BaseUrlTransport : UnitTestTransport { std::string expected_url; std::optional redirect_url; bool location_requested = false; @@ -3881,22 +3633,9 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { void send_request_to_server(const Request& request, util::UniqueFunction&& completion) override { - if (request.url.find("/login") != std::string::npos) { - CHECK(request.url.find(expected_url) != std::string::npos); - completion({200, 0, {}, user_json(good_access_token).dump()}); - } - else if (request.url.find("/profile") != std::string::npos) { - CHECK(request.url.find(expected_url) != std::string::npos); - completion({200, 0, {}, user_profile_json().dump()}); - } - else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) { - nlohmann::json json{{"access_token", good_access_token}}; - CHECK(request.url.find(expected_url) != std::string::npos); - completion({200, 0, {}, json.dump()}); - } - else if (request.url.find("/location") != std::string::npos) { + if (request.url.find("/location") != std::string::npos) { CHECK(request.method == HttpMethod::get); - CHECK(request.url.find(expected_url) != std::string::npos); + CHECK_THAT(request.url, ContainsSubstring(expected_url)); location_requested = true; if (location_returns_error) { completion(app::Response{static_cast(sync::HTTPStatus::NotFound), 0, {}, "404 not found"}); @@ -3921,32 +3660,20 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":" "\"%1\",\"ws_hostname\":\"%2\"}", expected_url, ws_url)}); + return; } + + UnitTestTransport::send_request_to_server(request, std::move(completion)); } }; - std::unique_ptr app_session; - auto redir_transport = std::make_shared(); - AutoVerifiedEmailCredentials creds; auto logger = util::Logger::get_default_logger(); - App::Config app_config = {"fake-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::NoEncryption; - sc_config.logger_factory = [](util::Logger::Level) { - return util::Logger::get_default_logger(); - }; - - auto do_login = [&](std::shared_ptr app) { - CHECK(app); - app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password), - [](std::shared_ptr user, util::Optional error) { - REQUIRE(user); - REQUIRE(!error); - }); + 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::create_ws_host_url") { @@ -3992,17 +3719,19 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { SECTION("Test app config baseurl") { { + // First time through, base_url is empty; https://services.cloud.mongodb.com is expected redir_transport->reset(App::default_base_url()); + auto config = get_config_with_base_url(); + OfflineAppSession oas(config); + auto app = oas.app(); - // First time through, base_url is empty; https://services.cloud.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() == App::default_base_url()); CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url())); - do_login(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == App::default_base_url()); CHECK(app->get_host_url() == App::default_base_url()); @@ -4010,17 +3739,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"); - do_login(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"); @@ -4029,19 +3759,20 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { { // Third time through, base_url is not set, expect https://services.cloud.mongodb.com, // since metadata is no longer used - app_config.base_url = util::none; std::string expected_url = std::string(App::default_base_url()); std::string expected_wsurl = App::create_ws_host_url(App::default_base_url()); 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); - do_login(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == expected_url); CHECK(app->get_host_url() == expected_url); @@ -4049,17 +3780,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"); - do_login(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"); @@ -4070,115 +3802,87 @@ 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 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); - - do_login(app); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == "https://alternate.someurl.fake"); - CHECK(app->get_host_url() == "https://alternate.someurl.fake"); - CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); - - redir_transport->reset(App::default_base_url()); + 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(); - // Revert the base URL to the default URL value using std::nullopt - app->update_base_url(std::nullopt, [](util::Optional error) { - CHECK(!error); - }); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == App::default_base_url()); - CHECK(app->get_host_url() == App::default_base_url()); - CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url())); - // Expected URL is still App::default_base_url - do_login(app); + // Location is not requested until first app services request + CHECK(!redir_transport->location_requested); - redir_transport->reset("http://some-other.url.fake"); - app->update_base_url("http://some-other.url.fake", [](util::Optional error) { - CHECK(!error); - }); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == "http://some-other.url.fake"); - CHECK(app->get_host_url() == "http://some-other.url.fake"); - CHECK(app->get_ws_host_url() == "ws://some-other.url.fake"); - // Expected URL is still "http://some-other.url.fake" - do_login(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"); + CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); - redir_transport->reset(App::default_base_url()); + redir_transport->reset(App::default_base_url()); - // Revert the base URL to the default URL value using the empty string - app->update_base_url("", [](util::Optional error) { - CHECK(!error); - }); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == App::default_base_url()); - CHECK(app->get_host_url() == App::default_base_url()); - CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url())); - // Expected URL is still App::default_base_url - do_login(app); - } + // Revert the base URL to the default URL value using the empty string + app->update_base_url("", [](util::Optional error) { + CHECK(!error); + }); + CHECK(redir_transport->location_requested); + CHECK(app->get_base_url() == App::default_base_url()); + CHECK(app->get_host_url() == App::default_base_url()); + CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url())); + oas.make_user(); } SECTION("Test update_baseurl with redirect") { - { - app_config.base_url = "https://alternate.someurl.fake"; - redir_transport->reset("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); + // Location is not requested until first app services request + CHECK(!redir_transport->location_requested); - do_login(app); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == "https://alternate.someurl.fake"); - CHECK(app->get_host_url() == "https://alternate.someurl.fake"); - CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); + 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"); + CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); - redir_transport->reset("http://some-other.someurl.fake", "https://redirect.otherurl.fake"); + redir_transport->reset("http://some-other.someurl.fake", "https://redirect.otherurl.fake"); - app->update_base_url("http://some-other.someurl.fake", [](util::Optional error) { - CHECK(!error); - }); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == "http://some-other.someurl.fake"); - 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 - do_login(app); - } + app->update_base_url("http://some-other.someurl.fake", [](util::Optional error) { + CHECK(!error); + }); + CHECK(redir_transport->location_requested); + CHECK(app->get_base_url() == "http://some-other.someurl.fake"); + 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 + oas.make_user(); } SECTION("Test update_baseurl returns error") { - { - app_config.base_url = "http://alternate.someurl.fake"; - redir_transport->reset("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); + // Location is not requested until first app services request + CHECK(!redir_transport->location_requested); - do_login(app); - CHECK(redir_transport->location_requested); - CHECK(app->get_base_url() == "http://alternate.someurl.fake"); - CHECK(app->get_host_url() == "http://alternate.someurl.fake"); - CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake"); + 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"); + CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake"); - redir_transport->reset("https://some-other.someurl.fake"); - redir_transport->location_returns_error = true; + redir_transport->reset("https://some-other.someurl.fake"); + redir_transport->location_returns_error = true; - app->update_base_url("https://some-other.someurl.fake", [](util::Optional error) { - CHECK(error); - }); - CHECK(redir_transport->location_requested); - // Verify original url values are still being used - CHECK(app->get_base_url() == "http://alternate.someurl.fake"); - CHECK(app->get_host_url() == "http://alternate.someurl.fake"); - CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake"); - } + app->update_base_url("https://some-other.someurl.fake", [](util::Optional error) { + CHECK(error); + }); + CHECK(redir_transport->location_requested); + // Verify original url values are still being used + CHECK(app->get_base_url() == "http://alternate.someurl.fake"); + CHECK(app->get_host_url() == "http://alternate.someurl.fake"); + CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake"); } // Verify new sync session updates location when created with cached user @@ -4198,60 +3902,76 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found"); }; - sc_config.socket_provider = socket_provider; - - 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(); { auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(init_url)) != std::string::npos); + CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url))); CHECK_FALSE(verified); } - do_login(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == init_url); CHECK(app->get_ws_host_url() == init_wsurl); - { - auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(init_url)) != std::string::npos); - CHECK(verified); - } + auto [sync_route, verified] = app->sync_manager()->sync_route(); + CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url))); + CHECK_THAT(sync_route, ContainsSubstring(init_wsurl)); + CHECK(verified); } - // Recreate the app using the cached user and start a sync session, which is set to fail on connect + + // 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 after updating location") { 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(); + // Verify the default sync route, which has not been verified { auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(init_url)) != std::string::npos); + CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url))); CHECK_FALSE(verified); } - - socket_provider->endpoint_verify_func = [&use_ssl, &expected_host, - &expected_port](const sync::WebSocketEndpoint& ep) { - CHECK(ep.address == expected_host); - CHECK(ep.port == expected_port); - CHECK(ep.is_ssl == use_ssl); + REQUIRE(app->current_user()); + + std::atomic connect_attempts = 0; + socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) { + // First connection attempt is to the originally specified endpoint. Since + // it hasn't been verified, we swallow the error and do a location update, + // which will then try to connect to the redir target + auto attempt = connect_attempts++; + if (attempt == 0) { + CHECK(ep.address == initial_host); + CHECK(ep.port == initial_port); + CHECK(ep.is_ssl == use_ssl); + } + else { + CHECK(ep.address == expected_host); + CHECK(ep.port == expected_port); + CHECK(ep.is_ssl == use_ssl); + } }; 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 { + r_config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) mutable { // Websocket is forcing a 404 failure so it won't actually start logger->debug("Received expected error: %1", error.status); CHECK(!error.status.is_ok()); @@ -4266,76 +3986,79 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == redir_url); CHECK(app->get_ws_host_url() == redir_wsurl); - { - auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(redir_url)) != std::string::npos); - CHECK(verified); - } + auto [sync_route, verified] = app->sync_manager()->sync_route(); + CHECK_THAT(sync_route, ContainsSubstring(redir_wsurl)); + CHECK(verified); } SECTION("Sync Session retries after initial location failure") { enum class TestState { start, location_failed, session_started }; TestingStateMachine state(TestState::start); - int retry_count = GENERATE(1, 3); + const int retry_count = GENERATE(1, 3); redir_transport->reset(init_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(); + REQUIRE(app->current_user()); // Verify the default sync route, which has not been verified { auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(init_url)) != std::string::npos); + CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url))); CHECK_FALSE(verified); } - socket_provider->endpoint_verify_func = [&use_ssl, &initial_host, - &initial_port](const sync::WebSocketEndpoint& ep) { + socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) { CHECK(ep.address == initial_host); CHECK(ep.port == initial_port); CHECK(ep.is_ssl == use_ssl); }; - socket_provider->websocket_connect_func = [&]() -> std::optional { - // Check these items prior to holding the lock in transition_with() - if (state.get() == TestState::start) { - logger->debug("State: start"); - // Verify the location update failed + socket_provider->websocket_connect_func = [&, request_count = + 0]() mutable -> std::optional { + if (request_count == 0) { + // First connection attempt is to the unverified initial URL + // since we have a valid access token but have never successfully + // connected. This failing will trigger a location update. + CHECK_FALSE(redir_transport->location_requested); + } + else { + // All attempts after the first should have requested location CHECK(redir_transport->location_requested); + redir_transport->location_requested = false; + } + + // Until we allow a location request to succeed we should keep + // getting the original unverified route + if (redir_transport->location_returns_error) { CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == init_url); CHECK(app->get_ws_host_url() == init_wsurl); { auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(init_url)) != std::string::npos); + CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url))); CHECK_FALSE(verified); } } - state.transition_with([&](TestState cur_state) -> std::optional { - if (cur_state == TestState::start) { - // After number of location verify attempts has passed, let the location succeed - if (--retry_count <= 0) { - redir_transport->reset(init_url, redir_url); - socket_provider->endpoint_verify_func = - [&use_ssl, &expected_host, &expected_port](const sync::WebSocketEndpoint& ep) { - CHECK(ep.address == expected_host); - CHECK(ep.port == expected_port); - CHECK(ep.is_ssl == use_ssl); - }; - return TestState::location_failed; - } - redir_transport->location_requested = false; - } - return std::nullopt; - }); + // After the chosen number of attempts let the location request succeed + if (request_count++ >= retry_count) { + redir_transport->reset(init_url, redir_url); + socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) { + CHECK(ep.address == expected_host); + CHECK(ep.port == expected_port); + CHECK(ep.is_ssl == use_ssl); + state.transition_to(TestState::location_failed); + }; + } return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found"); }; 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 = [&](std::shared_ptr, SyncError error) mutable { // An error will only be reported if the websocket fails after updating the location and access token @@ -4355,15 +4078,12 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { auto realm = Realm::get_shared_realm(r_config); state.wait_for(TestState::session_started); - CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == redir_url); CHECK(app->get_ws_host_url() == redir_wsurl); - { - auto [sync_route, verified] = app->sync_manager()->sync_route(); - CHECK(sync_route.find(app::App::create_ws_host_url(redir_url)) != std::string::npos); - CHECK(verified); - } + auto [sync_route, verified] = app->sync_manager()->sync_route(); + CHECK_THAT(sync_route, ContainsSubstring(redir_wsurl)); + CHECK(verified); } } } @@ -4396,12 +4116,12 @@ TEST_CASE("app: custom user data integration tests", "[sync][app][user][function TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") { TestAppSession session; auto app = session.app(); - auto jwt = create_jwt(session.app()->config().app_id); + auto jwt = create_jwt(session.app()->app_id()); 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) { @@ -4496,13 +4216,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")); @@ -4689,7 +4409,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"); @@ -4755,10 +4475,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!"); @@ -4840,23 +4558,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); @@ -4868,7 +4585,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 @@ -4878,7 +4594,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); @@ -4909,14 +4625,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(); @@ -4970,9 +4685,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 = [=] { @@ -4991,28 +4705,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 @@ -5026,8 +4740,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); @@ -5037,14 +4751,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); @@ -5068,7 +4782,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); }); @@ -5086,14 +4800,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); @@ -5149,7 +4863,7 @@ TEST_CASE("app: response error handling", "[sync][app]") { CHECK(!error.is_service_error()); CHECK(error.is_http_error()); CHECK(*error.additional_status_code == 404); - CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); + CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal")); } SECTION("http 500") { response.http_status_code = 500; @@ -5159,7 +4873,7 @@ TEST_CASE("app: response error handling", "[sync][app]") { CHECK(!error.is_service_error()); CHECK(error.is_http_error()); CHECK(*error.additional_status_code == 500); - CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos); + CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal")); CHECK(error.link_to_server_logs.empty()); } @@ -5354,7 +5068,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); @@ -5619,18 +5333,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); @@ -5650,7 +5362,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); @@ -5666,12 +5378,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()); @@ -5684,7 +5398,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(); @@ -5710,13 +5423,12 @@ 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 == ""); CHECK(req.headers == Headers{{"Accept", "text/event-stream"}}); CHECK(req.timeout_ms == timeout_ms); - CHECK(req.uses_refresh_token == false); auto req_args = get_request_args(req); CHECK(req_args["name"] == "func"); @@ -5739,9 +5451,9 @@ TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { auto req = make_request(nullptr, ">>>>>?????"); CHECK(req.url.find('&') == std::string::npos); - CHECK(req.url.find("%2B") != std::string::npos); // + (from >) - CHECK(req.url.find("%2F") != std::string::npos); // / (from ?) - CHECK(req.url.find("%3D") != std::string::npos); // = (tail padding) + CHECK_THAT(req.url, ContainsSubstring("%2B")); // + (from >) + CHECK_THAT(req.url, ContainsSubstring("%2F")); // / (from ?) + CHECK_THAT(req.url, ContainsSubstring("%3D")); // = (tail padding) CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding) } SECTION("with user") { @@ -5756,7 +5468,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); @@ -5768,7 +5480,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"}, @@ -5794,12 +5506,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"; @@ -5816,10 +5525,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); @@ -5828,9 +5537,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 7d5f80617f4..d501afd76bb 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..1ef56f4819f 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.string(); + 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 9ba6e1688ef..b3f14ff61b0 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 { @@ -2678,17 +2678,19 @@ TEST_CASE("flx: connect to PBS as FLX returns an error", "[sync][flx][protocol][ } TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][flx][token][baas]") { - auto transport = std::make_shared(); + 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; @@ -2697,7 +2699,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; @@ -3426,7 +3428,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")); @@ -3517,8 +3519,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 a9703405f1e..dfde81dfaf4 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,20 +16,17 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include -#include - -#include -#include -#include +#include #include -#include -#include - +#include +#include +#include #include #include +#include +#include + #include #if REALM_PLATFORM_APPLE @@ -38,23 +35,32 @@ #endif 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 { -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"; -static const auto no_encryption = [] { - SyncClientConfig config; - config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; - return config; -}(); +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"; +constexpr const char* app_id = "app 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)); +} #if REALM_PLATFORM_APPLE -static constexpr const char* app_id = "app id"; -static constexpr const char* access_group = ""; -static bool can_access_keychain() +using realm::util::adoptCF; +using realm::util::CFPtr; + +constexpr const char* access_group = ""; +bool can_access_keychain() { static bool can_access_keychain = [] { bool can_access = keychain::create_new_metadata_realm_key(app_id, access_group) != none; @@ -112,404 +118,642 @@ std::vector generate_key() #endif // REALM_PLATFORM_APPLE } // anonymous namespace -TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, no_encryption, "app id"); - - 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); - } +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 + +using Strings = std::vector; -TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { +TEST_CASE("app metadata: common", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, no_encryption, "app id"); - const std::string provider_type = "https://realm.example.org"; - 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)); + 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: file action metadata", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, no_encryption, "app id"); - - 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("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("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"); + } + + 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, no_encryption, "app id"); - 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, no_encryption, "app id"); - 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, no_encryption, "app id"); - 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, no_encryption, "app id"); - 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"}); + } + + 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, ""); + } + + 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)); + } + +#if REALM_PLATFORM_APPLE + 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); + } + + 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 + + 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") { + { + 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); + } + + // Create some files in the user's directory without tracking them + auto path_1 = file_manager.realm_file_path(user_id, {}, "file 1", "partition 1"); + auto path_2 = file_manager.realm_file_path(user_id, {}, "file 2", "partition 2"); + File{path_1, File::mode_Write}; + File{path_2, File::mode_Write}; + + // Files should be deleted on next start since the user has been removed + create_metadata_store(config, file_manager); + CHECK_FALSE(File::exists(path_1)); + CHECK_FALSE(File::exists(path_2)); } } -TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { +TEST_CASE("app metadata: encryption", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncClientConfig config; - const auto identity0 = "identity0"; - SECTION("prohibits opening the metadata Realm with different keys") { - SECTION("different keys") { - { - // Open metadata realm, make metadata - config.custom_encryption_key = make_test_encryption_key(10); - SyncMetadataManager manager0(metadata_path, config, "app id"); - - 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 - config.custom_encryption_key = make_test_encryption_key(11); - SyncMetadataManager manager1(metadata_path, config, "app id"); - - 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("different encryption settings") { - { - // Encrypt metadata realm at path, make metadata - config.custom_encryption_key = make_test_encryption_key(10); - SyncMetadataManager manager0(metadata_path, config, "app id"); - - 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 - config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; - SyncMetadataManager manager1(metadata_path, config, "app id"); - 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("works when enabled") { - config.custom_encryption_key = make_test_encryption_key(10); - const auto identity = "testcase5a"; - SyncMetadataManager manager(metadata_path, config, "app id"); - 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, config, "app id"); - 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 - if (!can_access_keychain()) { - return; + + 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; } - auto delete_key = util::make_scope_exit([]() noexcept { - keychain::delete_metadata_realm_encryption_key(app_id, access_group); - }); - SyncClientConfig config; + else { + realm_config.encryption_key.clear(); + } + CHECK_NOTHROW(Realm::get_shared_realm(realm_config)); + }; - SECTION("automatically generates an encryption key for new files") { - { - SyncMetadataManager manager(metadata_path, config, app_id); - manager.set_current_user_identity(identity0); - } + 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); + } - // Should be able to reopen and read data - { - SyncMetadataManager manager(metadata_path, config, app_id); - REQUIRE(manager.get_current_user_identity() == identity0); - } + 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); - // Verify that the file is actually encrypted - REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, - Catch::Matchers::ContainsSubstring("invalid mnemonic")); + // 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); - SECTION("leaves existing unencrypted files unencrypted") { - { - config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; - SyncMetadataManager manager(metadata_path, config, app_id); - manager.set_current_user_identity(identity0); - } - { - config.metadata_mode = SyncClientConfig::MetadataMode::Encryption; - SyncMetadataManager manager(metadata_path, config, app_id); - REQUIRE(manager.get_current_user_identity() == identity0); - } - REQUIRE_NOTHROW(Group(metadata_path)); + // 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); - SECTION("recreates the file if the old encryption key was lost") { - { - SyncMetadataManager manager(metadata_path, config, app_id); - manager.set_current_user_identity(identity0); - } + // 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); + } - keychain::delete_metadata_realm_encryption_key(app_id, access_group); +#if REALM_PLATFORM_APPLE + if (!can_access_keychain()) { + return; + } + auto delete_key = util::make_scope_exit([&]() noexcept { + keychain::delete_metadata_realm_encryption_key(config.app_id, config.security_access_group); + }); - { - // File should now be missing the data - SyncMetadataManager manager(metadata_path, config, app_id); - 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); + } + auto key = keychain::get_existing_metadata_realm_key(config.app_id, config.security_access_group); + REQUIRE(key); + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); } + open_realm_with_key(key); + } - SECTION("invalid access group throws an error") { - config.security_access_group = "invalid"; - REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, config, app_id), InvalidArgument, - "Invalid access group 'invalid'. Make sure that you have added the access group to " - "your app's Keychain Access Groups Entitlement."); + 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); } -#else - REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, SyncClientConfig(), "app id"), InvalidArgument, + + 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)) { @@ -535,7 +779,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"}}); @@ -574,16 +818,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, no_encryption, "app id"); - 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(); @@ -606,53 +847,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, no_encryption, "app id"); - 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, no_encryption, "app id"); - 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, no_encryption, "app id"); + 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/progress_notifications.cpp b/test/object-store/sync/session/progress_notifications.cpp index 60766ffa665..0c2a5f1dd3d 100644 --- a/test/object-store/sync/session/progress_notifications.cpp +++ b/test/object-store/sync/session/progress_notifications.cpp @@ -647,8 +647,7 @@ struct PBS : TestSetup { SyncTestFile make_config() override { - const auto schema = get_default_schema(); - return SyncTestFile(session.app(), partition, schema); + return SyncTestFile(session.app()->current_user(), partition, get_default_schema()); } AnyDict make_one(int64_t /* idx */) override @@ -666,7 +665,7 @@ struct FLX : TestSetup { FLX(const std::string& app_id = "flx_sync_progress") : harness(app_id) { - table_name = (*harness.schema().begin()).name; + table_name = harness.schema().begin()->name; } SyncTestFile make_config() override diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index ad2a3e6221c..3e1c21bb35f 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); } @@ -366,7 +369,7 @@ TEST_CASE("sync: error handling", "[sync][session]") { }; SECTION("reports DNS error") { - tsm.sync_manager()->set_sync_route("ws://invalid.com:9090"); + tsm.sync_manager()->set_sync_route("ws://invalid.com:9090", true); auto user = tsm.fake_user(); auto session = sync_session(user, "/test", store_sync_error); @@ -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()->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 2894ea8bd6f..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,531 +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. - SyncClientConfig client_config; - client_config.metadata_mode = config.metadata_mode; - SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); - - 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. - SyncClientConfig client_config; - client_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); - - 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); @@ -712,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); @@ -738,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()); @@ -756,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 de3f697d2de..00000000000 --- a/test/object-store/sync/user.cpp +++ /dev/null @@ -1,305 +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. - SyncClientConfig client_config; - client_config.metadata_mode = tsm_config.metadata_mode; - SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); - - 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.cpp b/test/object-store/util/sync/baas_admin_api.cpp index 41dd4337f8d..3dba7bf0a79 100644 --- a/test/object-store/util/sync/baas_admin_api.cpp +++ b/test/object-store/util/sync/baas_admin_api.cpp @@ -358,8 +358,8 @@ app::Response do_http_request(const app::Request& request) return util::format("BaaS Coid: \"%1\"", coid_header->second); }(); - logger->trace("Baas API %1 request to %2 took %3 %4\n", app::httpmethod_to_string(request.method), - request.url, std::chrono::duration_cast(total_time), coid); + logger->trace("Baas API %1 request to %2 took %3 %4\n", request.method, request.url, + std::chrono::duration_cast(total_time), coid); } int http_code = 0; @@ -610,7 +610,7 @@ class BaasaasLauncher : public Catch::EventListenerBase { void testRunEnded(Catch::TestRunStats const&) override { - if (auto& baasaas_holder = get_baasaas_holder(); baasaas_holder.has_value()) { + if (auto& baasaas_holder = get_baasaas_holder()) { baasaas_holder->stop(); } } diff --git a/test/object-store/util/sync/baas_admin_api.hpp b/test/object-store/util/sync/baas_admin_api.hpp index c45a150bb45..46253a60f7e 100644 --- a/test/object-store/util/sync/baas_admin_api.hpp +++ b/test/object-store/util/sync/baas_admin_api.hpp @@ -268,7 +268,7 @@ std::string get_base_url(); std::string get_admin_url(); 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 5bec44aea65..762c8d66eef 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(); @@ -466,7 +445,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 @@ -497,7 +476,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 89046211345..954d10e3d8c 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 #include @@ -49,9 +49,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)); @@ -144,13 +141,6 @@ std::ostream& operator<<(std::ostream& os, util::Optional error); void subscribe_to_all_and_bootstrap(Realm& realm); -#if REALM_ENABLE_AUTH_TESTS -void wait_for_sessions_to_close(const TestAppSession& test_app_session); - -std::string get_compile_time_base_url(); -std::string get_compile_time_admin_url(); -#endif // REALM_ENABLE_AUTH_TESTS - struct AutoVerifiedEmailCredentials : app::AppCredentials { AutoVerifiedEmailCredentials(); std::string email; @@ -162,6 +152,13 @@ AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); // when calling create_user_and_log_in() void log_in_user(app::SharedApp app, app::AppCredentials creds); +#if REALM_ENABLE_AUTH_TESTS +void wait_for_sessions_to_close(const TestAppSession& test_app_session); + +std::string get_compile_time_base_url(); +std::string get_compile_time_admin_url(); +#endif // REALM_ENABLE_AUTH_TESTS + void wait_for_advance(Realm& realm); void async_open_realm(const Realm::Config& config, @@ -193,8 +190,10 @@ class SynchronousTestTransport : public app::GenericNetworkTransport { std::mutex m_mutex; }; +template +class HookedTransport : public BaseTransport { + static_assert(std::is_base_of_v); -class HookedTransport : public SynchronousTestTransport { public: void send_request_to_server(const app::Request& request, util::UniqueFunction&& completion) override @@ -204,7 +203,7 @@ class HookedTransport : public SynchronousTestTransport { return completion(*simulated_response); } } - SynchronousTestTransport::send_request_to_server(request, [&](const app::Response& response) mutable { + BaseTransport::send_request_to_server(request, [&](app::Response response) mutable { if (response_hook) { response_hook(request, response); } @@ -213,9 +212,9 @@ class HookedTransport : public SynchronousTestTransport { } // Optional handler for the request and response before it is returned to completion - std::function response_hook; + util::UniqueFunction response_hook; // Optional handler for the request before it is sent to the server - std::function(const app::Request&)> request_hook; + util::UniqueFunction(const app::Request&)> request_hook; }; @@ -248,7 +247,7 @@ struct SocketProviderError { struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider { - HookedSocketProvider(const std::shared_ptr& logger, const std::string user_agent, + HookedSocketProvider(const std::shared_ptr& logger, const std::string& user_agent, AutoStart auto_start = AutoStart{true}) : DefaultSocketProvider(logger, user_agent, nullptr, auto_start) { @@ -263,7 +262,7 @@ struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider { } if (websocket_endpoint_resolver) { - endpoint = websocket_endpoint_resolver(std::move(endpoint)); + websocket_endpoint_resolver(endpoint); } if (websocket_connect_func) { @@ -286,9 +285,9 @@ struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider { return websocket; } - std::function websocket_endpoint_resolver; - std::function endpoint_verify_func; - std::function()> websocket_connect_func; + util::UniqueFunction websocket_endpoint_resolver; + util::UniqueFunction endpoint_verify_func; + util::UniqueFunction()> websocket_connect_func; }; #endif // REALM_ENABLE_SYNC @@ -349,10 +348,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 e4365668f07..30c1fe7ade8 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 @@ -131,11 +133,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)) { @@ -190,8 +187,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)) { } @@ -249,6 +246,11 @@ std::string SyncServer::url_for_realm(StringData realm_name) const return util::format("%1/%2", m_url, realm_name); } +int SyncServer::port() const +{ + return m_server.listen_endpoint().port(); +} + struct WaitForSessionState { std::condition_variable cv; std::mutex mutex; @@ -260,7 +262,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(); @@ -292,7 +294,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) @@ -315,6 +317,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 @@ -339,18 +342,18 @@ TestAppSession::TestAppSession(AppSession session, m_transport = instance_of; app_config = get_config(m_transport, *m_app_session); 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); - 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(); @@ -359,58 +362,22 @@ TestAppSession::TestAppSession(AppSession session, TestAppSession::~TestAppSession() { - close(true); - if (m_delete_app) { - m_app_session->admin_api.delete_app(m_app_session->server_app_id); - } -} - -void TestAppSession::close(bool tear_down) -{ - try { - if (tear_down) { - // If tearing down, make sure there's an app to work with - if (!m_app) { - reopen(false); - } - REALM_ASSERT(m_app); - // Clean up the app data + if (util::File::exists(m_base_file_path)) { + try { m_app->sync_manager()->tear_down_for_testing(); - } - else if (m_app) { - // Otherwise, make sure all the session are closed - m_app->sync_manager()->close_all_sessions(); - } - m_app.reset(); - - // If tearing down, clean up the test file directory - if (tear_down && !m_base_file_path.empty() && util::File::exists(m_base_file_path)) { util::try_remove_dir_recursive(m_base_file_path); - m_base_file_path.clear(); } + catch (const std::exception& ex) { + std::cerr << ex.what() << "\n"; + } + app::App::clear_cached_apps(); } - catch (const std::exception& ex) { - std::cerr << "Error tearing down TestAppSession: " << ex.what() << "\n"; - } - // Ensure all cached apps are cleared - app::App::clear_cached_apps(); -} - -void TestAppSession::reopen(bool log_in) -{ - // These are REALM_ASSERTs so the test crashes if this object is in a bad state - REALM_ASSERT(!m_base_file_path.empty()); - REALM_ASSERT(!m_app); - m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); - - // initialize sync client - m_app->sync_manager()->get_sync_client(); - if (log_in) { - log_in_user(m_app, user_creds); + if (m_delete_app) { + m_app_session->admin_api.delete_app(m_app_session->server_app_id); } } -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"); @@ -457,16 +424,14 @@ std::vector TestAppSession::get_documents(SyncUser& user, co 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", true); if (config.start_sync_client) { m_sync_manager->get_sync_client(); } @@ -488,9 +453,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) @@ -503,6 +471,9 @@ 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); + if (config.storage_path) { m_base_file_path = *config.storage_path; util::try_make_dir(m_base_file_path); @@ -511,21 +482,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; - - 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() @@ -542,9 +508,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 cf9692c179d..f41e7b16a82 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -113,6 +113,7 @@ class SyncServer : private realm::sync::Clock { std::string local_dir; }; + SyncServer(const Config& config); ~SyncServer(); void start(); @@ -127,6 +128,7 @@ class SyncServer : private realm::sync::Clock { { return m_local_root_dir; } + int port() const; template void advance_clock(std::chrono::duration duration = std::chrono::seconds(1)) noexcept @@ -135,8 +137,6 @@ class SyncServer : private realm::sync::Clock { } private: - friend class TestSyncManager; - SyncServer(const Config& config); std::string m_local_root_dir; std::shared_ptr m_logger; realm::sync::Server m_server; @@ -150,6 +150,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 @@ -169,7 +238,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); }; @@ -178,7 +247,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; bool start_sync_client = true; }; @@ -199,13 +267,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 { @@ -215,7 +283,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; @@ -227,7 +295,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(); @@ -276,26 +344,12 @@ class TestAppSession { return m_app->sync_manager(); } - void close() - { - close(false); - } - - // Re-open the app without deleting the dir contents - if close() has not been called - // the App will be closed first before recreating the object. - // If log_in is true, user will be logged in again once the App instance is created - void reopen(bool log_in); + realm::app::AppConfig app_config; - realm::app::App::Config app_config; - realm::SyncClientConfig sc_config; - - 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: - // Close the app and, if tear_down, remove the app data and base_file_path directory - void close(bool tear_down); - std::shared_ptr m_app; std::unique_ptr m_app_session; std::string m_base_file_path; @@ -309,7 +363,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 8c471159b0b..f9174deeaa2 100644 --- a/test/object-store/util/unit_test_transport.cpp +++ b/test/object-store/util/unit_test_transport.cpp @@ -18,6 +18,8 @@ #include "util/unit_test_transport.hpp" +#include "util/test_utils.hpp" + #include #include #include @@ -28,11 +30,7 @@ using namespace realm; using namespace realm::app; -std::string UnitTestTransport::access_token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi" - "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk" - "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s"; +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 cbee53d1941..d409e955d91 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; diff --git a/test/test_util_websocket.cpp b/test/test_util_websocket.cpp index 82df3ee931a..9f45f0609bf 100644 --- a/test/test_util_websocket.cpp +++ b/test/test_util_websocket.cpp @@ -223,7 +223,7 @@ class WSConfig : public websocket::Config { n_write_errors++; } - void websocket_handshake_error_handler(std::error_code, const HTTPHeaders*, const std::string_view*) override + void websocket_handshake_error_handler(std::error_code, const HTTPHeaders*, std::string_view) override { n_protocol_errors++; } diff --git a/test/tsan.suppress b/test/tsan.suppress index 4ae53f615b6..987a4364332 100644 --- a/test/tsan.suppress +++ b/test/tsan.suppress @@ -24,3 +24,8 @@ race:adjtime # on the sync thread for async open task, should be harmless to suppress, # but ultimately needs to be fixed: #7083 race:uv_async_init + +# We try to shut down the remote BaaSaaS instance from inside a signal handler, +# which invovles a bunch of memory allocations. This is a pretty unsafe thing +# to do, but we want to avoid leaking server resources. +signal:realm::Baasaas::stop