diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2eea95227..07484d29bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Audit event scopes containing zero events to save no longer open the audit realm unneccesarily ([PR #7332](https://github.com/realm/realm-core/pull/7332)). * Added a method to check if a file needs upgrade. ([#7140](https://github.com/realm/realm-core/issues/7140)) * Use `clonefile()` when possible in `File::copy()` on Apple platforms for faster copying. ([PR #7341](https://github.com/realm/realm-core/pull/7341)). +* 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 * Fixed queries like `indexed_property == NONE {x}` which mistakenly matched on only x instead of not x. This only applies when an indexed property with equality (==, or IN) matches with `NONE` on a list of one item. If the constant list contained more than one value then it was working correctly. ([realm-js #7862](https://github.com/realm/realm-java/issues/7862), since v12.5.0) @@ -20,9 +21,23 @@ * 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)). +* 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). ### 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)) +* The following types have been renamed 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 + ([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). +* Getting sync sessions from a sync user is now done via SyncManager::get_all_sessions_for() rather than SyncUser::all_sessions(). ([PR #7300](https://github.com/realm/realm-core/pull/7300). ### Compatibility * Fileformat: Generates files with format v23. Reads and automatically upgrade from fileformat v5. @@ -37,6 +52,9 @@ * The minimum CMake version has changed from 3.15 to 3.22.1. ([#6537](https://github.com/realm/realm-core/issues/6537)) * Update Catch2 to v3.5.2 ([PR #7297](https://github.com/realm/realm-core/pull/7297)). * The unused `partition` and `user_local_uuid()` fields have been removed from `FileActionMetadata`. ([PR #7341](https://github.com/realm/realm-core/pull/7341)). +* App metadaa storage has been entirely rewritten in preparation for supporting sharing metadata realms between processes. +* 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 ownership relationship between App and User has changed. User now strongly retains App and App has a weak cache of Users. ---------------------------------------------- diff --git a/bindgen/spec.yml b/bindgen/spec.yml index 04e24cc3423..2c7ce3bdf90 100644 --- a/bindgen/spec.yml +++ b/bindgen/spec.yml @@ -221,7 +221,7 @@ enums: - FUNCTION - API_KEY MetadataMode: - cppName: SyncClientConfig::MetadataMode + cppName: app::AppConfig::MetadataMode values: - NoEncryption - Encryption @@ -275,13 +275,18 @@ enums: - DeleteRealm - ClientReset - ClientResetNoRecovery + SyncFileAction: + cppName: SyncFileAction + values: + - DeleteRealm + - BackUpThenDeleteRealm ProgressDirection: cppName: SyncSession::ProgressDirection values: - upload - download SyncUserState: - cppName: SyncUser::State + cppName: UserState values: - LoggedOut - LoggedIn @@ -434,7 +439,7 @@ records: default: false UserIdentity: - cppName: SyncUserIdentity + cppName: app::UserIdentity fields: id: type: std::string @@ -557,11 +562,6 @@ records: SyncClientConfig: fields: - base_file_path: std::string - metadata_mode: - type: MetadataMode - default: MetadataMode::Encryption - custom_encryption_key: std::optional logger_factory: Nullable log_level: type: LoggerLevel @@ -620,7 +620,7 @@ records: body: std::string DeviceInfo: - cppName: app::App::Config::DeviceInfo + cppName: app::AppConfig::DeviceInfo fields: platform_version: std::string sdk_version: std::string @@ -632,13 +632,19 @@ records: bundle_id: std::string AppConfig: - cppName: app::App::Config + cppName: app::AppConfig fields: app_id: std::string transport: SharedGenericNetworkTransport base_url: std::optional default_request_timeout_ms: std::optional device_info: DeviceInfo + base_file_path: std::string + sync_client_config: SyncClientConfig + metadata_mode: + type: MetadataMode + default: MetadataMode::Encryption + custom_encryption_key: std::optional CompensatingWriteErrorInfo: cppName: sync::CompensatingWriteErrorInfo @@ -1132,36 +1138,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' @@ -1182,27 +1201,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 @@ -1212,8 +1231,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' @@ -1230,8 +1249,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 @@ -1247,12 +1266,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 @@ -1269,7 +1288,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)' @@ -1277,8 +1295,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 3c9cc937743..09c4c1b28cc 100644 --- a/src/realm.h +++ b/src/realm.h @@ -2794,6 +2794,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; @@ -2902,6 +2908,12 @@ RLM_API void realm_app_config_set_framework_name(realm_app_config_t* config, RLM_API void realm_app_config_set_framework_version(realm_app_config_t* config, const char* framework_version) RLM_API_NOEXCEPT; RLM_API void realm_app_config_set_bundle_id(realm_app_config_t* config, const char* bundle_id) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_base_file_path(realm_app_config_t*, const char*) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_metadata_mode(realm_app_config_t*, + realm_sync_client_metadata_mode_e) RLM_API_NOEXCEPT; +RLM_API void realm_app_config_set_metadata_encryption_key(realm_app_config_t*, const uint8_t[64]) RLM_API_NOEXCEPT; + +RLM_API realm_sync_client_config_t* realm_app_config_get_sync_client_config(realm_app_config_t*) RLM_API_NOEXCEPT; /** * Get an existing @a realm_app_credentials_t and return it's json representation @@ -2916,14 +2928,14 @@ RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credential * * @return A non-null pointer if no error occurred. */ -RLM_API realm_app_t* realm_app_create(const realm_app_config_t*, const realm_sync_client_config_t*); +RLM_API realm_app_t* realm_app_create(const realm_app_config_t*); /** * Create cached realm_app_t* instance given a valid realm configuration and sync client configuration. * * @return A non-null pointer if no error occurred. */ -RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t*, const realm_sync_client_config_t*); +RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t*); /** * Get a cached realm_app_t* instance given an app id. out_app may be null if the app with this id hasn't been @@ -3053,11 +3065,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. @@ -3292,9 +3303,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 @@ -3370,12 +3381,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, @@ -3511,7 +3516,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. @@ -3536,11 +3541,6 @@ typedef void (*realm_async_open_task_completion_func_t)(realm_userdata_t userdat typedef void (*realm_async_open_task_init_subscription_func_t)(realm_t* realm, realm_userdata_t userdata); RLM_API realm_sync_client_config_t* realm_sync_client_config_new(void) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_base_file_path(realm_sync_client_config_t*, const char*) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_metadata_mode(realm_sync_client_config_t*, - realm_sync_client_metadata_mode_e) RLM_API_NOEXCEPT; -RLM_API void realm_sync_client_config_set_metadata_encryption_key(realm_sync_client_config_t*, - const uint8_t[64]) RLM_API_NOEXCEPT; RLM_API void realm_sync_client_config_set_reconnect_mode(realm_sync_client_config_t*, realm_sync_client_reconnect_mode_e) RLM_API_NOEXCEPT; RLM_API void realm_sync_client_config_set_multiplex_sessions(realm_sync_client_config_t*, bool) RLM_API_NOEXCEPT; @@ -3919,7 +3919,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 4d7378d28c8..836ee9fe243 100644 --- a/src/realm/mixed.hpp +++ b/src/realm/mixed.hpp @@ -167,6 +167,10 @@ class Mixed { : Mixed(StringData(s)) { } + Mixed(std::string_view s) noexcept + : Mixed(StringData(s)) + { + } DataType get_type() const noexcept { diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index 1f564599715..2b91b998eb8 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -509,6 +509,12 @@ inline Obj& Obj::set(ColKey col_key, std::string str, bool is_default) return set(col_key, StringData(str), is_default); } +template <> +inline Obj& Obj::set(ColKey col_key, std::string_view str, bool is_default) +{ + return set(col_key, StringData(str), is_default); +} + template <> inline Obj& Obj::set(ColKey col_key, realm::null, bool is_default) { diff --git a/src/realm/object-store/CMakeLists.txt b/src/realm/object-store/CMakeLists.txt index 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 d7c6921ee5b..149a46318f4 100644 --- a/src/realm/object-store/audit.mm +++ b/src/realm/object-store/audit.mm @@ -622,20 +622,15 @@ bool write_event(Timestamp timestamp, StringData activity, StringData event_type // Get a pool for the given sync user. Pools are cached internally to avoid // creating duplicate ones. - static std::shared_ptr get_pool(std::shared_ptr user, - std::string const& partition_prefix, - const std::shared_ptr& logger, - ErrorHandler error_handler); + static std::shared_ptr get_pool(std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger); // Write to a pooled Realm. The Transaction should not be retained outside // of the callback. void write(util::FunctionRef func) REQUIRES(!m_mutex); - explicit AuditRealmPool(Private, std::shared_ptr user, std::string const& partition_prefix, - ErrorHandler error_handler, const std::shared_ptr& logger, - std::string_view app_id); - AuditRealmPool(const AuditRealmPool&) = delete; - AuditRealmPool& operator=(const AuditRealmPool&) = delete; + explicit AuditRealmPool(Private, std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger); // Block the calling thread until all pooled Realms have been fully uploaded, // including ones which do not currently have sync sessions. For testing @@ -663,13 +658,12 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con std::string prefixed_partition(std::string const& partition); }; -std::shared_ptr AuditRealmPool::get_pool(std::shared_ptr user, - std::string const& partition_prefix, - const std::shared_ptr& logger, - ErrorHandler error_handler) NO_THREAD_SAFETY_ANALYSIS +std::shared_ptr +AuditRealmPool::get_pool(std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger) NO_THREAD_SAFETY_ANALYSIS { struct CachedPool { - std::string user_identity; + std::string user_id; std::string partition_prefix; std::string app_id; std::weak_ptr pool; @@ -683,9 +677,9 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con }), s_pools.end()); - auto app_id = user->sync_manager()->app_id(); + auto app_id = user->app_id(); auto it = std::find_if(s_pools.begin(), s_pools.end(), [&](auto& pool) { - return pool.user_identity == user->identity() && pool.partition_prefix == partition_prefix && + return pool.user_id == user->user_id() && pool.partition_prefix == config.partition_value_prefix && pool.app_id == app_id; }); if (it != s_pools.end()) { @@ -694,28 +688,26 @@ explicit AuditRealmPool(Private, std::shared_ptr user, std::string con } } - auto pool = std::make_shared(Private(), user, partition_prefix, error_handler, logger, app_id); + auto pool = std::make_shared(Private(), user, config, logger); pool->scan_for_realms_to_upload(); - s_pools.push_back({user->identity(), partition_prefix, app_id, pool}); + s_pools.push_back({user->user_id(), config.partition_value_prefix, app_id, pool}); return pool; } -AuditRealmPool::AuditRealmPool(Private, std::shared_ptr user, std::string const& partition_prefix, - ErrorHandler error_handler, const std::shared_ptr& logger, - std::string_view app_id) +AuditRealmPool::AuditRealmPool(Private, std::shared_ptr user, const AuditConfig& config, + const std::shared_ptr& logger) : m_user(user) - , m_partition_prefix(partition_prefix) - , m_error_handler(error_handler) + , m_partition_prefix(config.partition_value_prefix) + , m_error_handler(config.sync_error_handler) , m_path_root([&] { - auto base_file_path = m_user->sync_manager()->config().base_file_path; #ifdef _WIN32 // Move to File? const char separator[] = "\\"; #else const char separator[] = "/"; #endif // "$root/realm-audit/$appId/$userId/$partitonPrefix/" - return util::format("%2%1realm-audit%1%3%1%4%1%5%1", separator, base_file_path, app_id, m_user->identity(), - partition_prefix); + return util::format("%2%1realm-audit%1%3%1%4%1%5%1", separator, config.base_file_path, m_user->app_id(), + m_user->user_id(), config.partition_value_prefix); }()) , m_logger(logger) { @@ -1015,8 +1007,7 @@ throw InvalidArgument("Auditing a flexible sync realm requires setting the audit if (!m_serializer) m_serializer = std::make_shared(); - m_realm_pool = AuditRealmPool::get_pool(audit_user, audit_config.partition_value_prefix, m_logger, - audit_config.sync_error_handler); + m_realm_pool = AuditRealmPool::get_pool(audit_user, audit_config, m_logger); } void AuditContext::update_metadata(std::vector> new_metadata) diff --git a/src/realm/object-store/c_api/app.cpp b/src/realm/object-store/c_api/app.cpp index d9d3430a393..6352910d01f 100644 --- a/src/realm/object-store/c_api/app.cpp +++ b/src/realm/object-store/c_api/app.cpp @@ -86,7 +86,7 @@ static inline auto make_callback(realm_app_user_completion_func_t callback, real realm_free_userdata_func_t userdata_free) { return [callback, userdata = SharedUserdata(userdata, FreeUserdata(userdata_free))]( - std::shared_ptr user, util::Optional error) { + std::shared_ptr user, util::Optional error) { if (error) { realm_app_error_t c_err{to_capi(*error)}; callback(userdata.get(), nullptr, &c_err); @@ -238,6 +238,22 @@ RLM_API void realm_app_config_set_bundle_id(realm_app_config_t* config, const ch config->device_info.bundle_id = std::string(bundle_id); } +RLM_API void realm_app_config_set_base_file_path(realm_app_config_t* config, const char* path) noexcept +{ + config->base_file_path = path; +} + +RLM_API void realm_app_config_set_metadata_mode(realm_app_config_t* config, + realm_sync_client_metadata_mode_e mode) noexcept +{ + config->metadata_mode = app::AppConfig::MetadataMode(mode); +} + +RLM_API void realm_app_config_set_metadata_encryption_key(realm_app_config_t* config, const uint8_t key[64]) noexcept +{ + config->custom_encryption_key = std::vector(key, key + 64); +} + RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credentials_t* app_credentials) noexcept { return wrap_err([&] { @@ -245,19 +261,17 @@ RLM_API const char* realm_app_credentials_serialize_as_json(realm_app_credential }); } -RLM_API realm_app_t* realm_app_create(const realm_app_config_t* app_config, - const realm_sync_client_config_t* sync_client_config) +RLM_API realm_app_t* realm_app_create(const realm_app_config_t* app_config) { return wrap_err([&] { - return new realm_app_t(App::get_app(app::App::CacheMode::Disabled, *app_config, *sync_client_config)); + return new realm_app_t(App::get_app(app::App::CacheMode::Disabled, *app_config)); }); } -RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t* app_config, - const realm_sync_client_config_t* sync_client_config) +RLM_API realm_app_t* realm_app_create_cached(const realm_app_config_t* app_config) { return wrap_err([&] { - return new realm_app_t(App::get_app(app::App::CacheMode::Enabled, *app_config, *sync_client_config)); + return new realm_app_t(App::get_app(app::App::CacheMode::Enabled, *app_config)); }); } @@ -344,22 +358,40 @@ RLM_API bool realm_app_log_out_current_user(realm_app_t* app, realm_app_void_com }); } +namespace { +template +auto with_app_user(const realm_user_t* user, Fn&& fn) +{ + auto app_user = std::dynamic_pointer_cast(*user); + return wrap_err([&] { + if (!app_user) { + throw Exception(ErrorCodes::InvalidArgument, "App Services function require a user obtained from an App"); + } + if constexpr (std::is_void_v) { + fn(app_user); + return true; + } + else { + return fn(app_user); + } + }); +} +} // anonymous namespace + RLM_API bool realm_app_refresh_custom_data(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->refresh_custom_data(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->refresh_custom_data(user, make_callback(callback, userdata, userdata_free)); }); } RLM_API bool realm_app_log_out(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->log_out(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->log_out(user, make_callback(callback, userdata, userdata_free)); }); } @@ -367,38 +399,31 @@ RLM_API bool realm_app_link_user(realm_app_t* app, realm_user_t* user, realm_app realm_app_user_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->link_user(*user, *credentials, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->link_user(user, *credentials, make_callback(callback, userdata, userdata_free)); }); } -RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user, realm_user_t** new_user) +RLM_API bool realm_app_switch_user(realm_app_t* app, realm_user_t* user) { - return wrap_err([&] { - auto new_user_local = (*app)->switch_user(*user); - if (new_user) { - *new_user = new realm_user_t(std::move(new_user_local)); - } - return true; + return with_app_user(user, [&](auto& user) { + (*app)->switch_user(user); }); } RLM_API bool realm_app_remove_user(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->remove_user(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->remove_user(user, make_callback(callback, userdata, userdata_free)); }); } RLM_API bool realm_app_delete_user(realm_app_t* app, realm_user_t* user, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { - (*app)->delete_user(*user, make_callback(callback, userdata, userdata_free)); - return true; + return with_app_user(user, [&](auto& user) { + (*app)->delete_user(user, make_callback(callback, userdata, userdata_free)); }); } @@ -492,10 +517,9 @@ RLM_API bool realm_app_user_apikey_provider_client_create_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().create_api_key( - name, *user, make_callback(callback, userdata, userdata_free)); - return true; + name, user, make_callback(callback, userdata, userdata_free)); }); } @@ -505,10 +529,9 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikey(const realm_app_ realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().fetch_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -517,7 +540,7 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikeys(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}]( std::vector apikeys, util::Optional error) { if (error) { @@ -534,8 +557,7 @@ RLM_API bool realm_app_user_apikey_provider_client_fetch_apikeys(const realm_app } }; - (*app)->provider_client().fetch_api_keys(*user, std::move(cb)); - return true; + (*app)->provider_client().fetch_api_keys(user, std::move(cb)); }); } @@ -545,10 +567,9 @@ RLM_API bool realm_app_user_apikey_provider_client_delete_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().delete_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -558,10 +579,9 @@ RLM_API bool realm_app_user_apikey_provider_client_enable_apikey(const realm_app realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().enable_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -571,10 +591,9 @@ RLM_API bool realm_app_user_apikey_provider_client_disable_apikey(const realm_ap realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app)->provider_client().disable_api_key( - from_capi(id), *user, make_callback(callback, userdata, userdata_free)); - return true; + from_capi(id), user, make_callback(callback, userdata, userdata_free)); }); } @@ -582,11 +601,10 @@ RLM_API bool realm_app_push_notification_client_register_device( const realm_app_t* app, const realm_user_t* user, const char* service_name, const char* registration_token, realm_app_void_completion_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app) ->push_notification_client(service_name) - .register_device(registration_token, *user, make_callback(callback, userdata, userdata_free)); - return true; + .register_device(registration_token, user, make_callback(callback, userdata, userdata_free)); }); } @@ -596,11 +614,10 @@ RLM_API bool realm_app_push_notification_client_deregister_device(const realm_ap realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { (*app) ->push_notification_client(service_name) - .deregister_device(*user, make_callback(callback, userdata, userdata_free)); - return true; + .deregister_device(user, make_callback(callback, userdata, userdata_free)); }); } @@ -609,7 +626,7 @@ RLM_API bool realm_app_call_function(const realm_app_t* app, const realm_user_t* realm_return_string_func_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}]( const std::string* reply, util::Optional error) { if (error) { @@ -622,8 +639,7 @@ RLM_API bool realm_app_call_function(const realm_app_t* app, const realm_user_t* }; util::Optional service_name_opt = service_name ? util::some(service_name) : util::none; - (*app)->call_function(*user, function_name, serialized_ejson_payload, service_name_opt, std::move(cb)); - return true; + (*app)->call_function(user, function_name, serialized_ejson_payload, service_name_opt, std::move(cb)); }); } @@ -645,17 +661,21 @@ RLM_API void realm_app_sync_client_wait_for_sessions_to_terminate(realm_app_t* a RLM_API char* realm_app_sync_client_get_default_file_path_for_realm(const realm_sync_config_t* config, const char* custom_filename) { - return wrap_err([&]() { + return wrap_err([&]() -> char* { + auto user = std::dynamic_pointer_cast(config->user); + if (!user) { + return nullptr; + } util::Optional filename = custom_filename ? util::some(custom_filename) : util::none; - std::string file_path = config->user->sync_manager()->path_for_realm(*config, std::move(filename)); + std::string file_path = user->app()->path_for_realm(*config, std::move(filename)); return duplicate_string(file_path); }); } -RLM_API const char* realm_user_get_identity(const realm_user_t* user) noexcept +RLM_API char* realm_user_get_identity(const realm_user_t* user) noexcept { - return (*user)->identity().c_str(); + return duplicate_string((*user)->user_id()); } RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) noexcept @@ -666,8 +686,8 @@ RLM_API realm_user_state_e realm_user_get_state(const realm_user_t* user) noexce RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_identity_t* out_identities, size_t max, size_t* out_n) { - return wrap_err([&] { - const auto& identities = (*user)->identities(); + return with_app_user(user, [&](auto& user) { + const auto& identities = user->identities(); set_out_param(out_n, identities.size()); if (out_identities && max >= identities.size()) { for (size_t i = 0; i < identities.size(); i++) { @@ -675,24 +695,24 @@ RLM_API bool realm_user_get_all_identities(const realm_user_t* user, realm_user_ realm_auth_provider_e(enum_from_provider_type(identities[i].provider_type))}; } } - return true; }); } RLM_API char* realm_user_get_device_id(const realm_user_t* user) noexcept { - if ((*user)->has_device_id()) { - return duplicate_string((*user)->device_id()); - } - - return nullptr; + char* device_id = nullptr; + with_app_user(user, [&](auto& user) { + if (user->has_device_id()) { + device_id = duplicate_string(user->device_id()); + } + }); + return device_id; } RLM_API bool realm_user_log_out(realm_user_t* user) { - return wrap_err([&] { - (*user)->log_out(); - return true; + return with_app_user(user, [&](auto& user) { + user->log_out(); }); } @@ -703,20 +723,21 @@ RLM_API bool realm_user_is_logged_in(const realm_user_t* user) noexcept RLM_API char* realm_user_get_profile_data(const realm_user_t* user) { - return wrap_err([&] { - std::string data = bson::Bson((*user)->user_profile().data()).to_string(); + return with_app_user(user, [&](auto& user) { + std::string data = bson::Bson(user->user_profile().data()).to_string(); return duplicate_string(data); }); } RLM_API char* realm_user_get_custom_data(const realm_user_t* user) noexcept { - if (const auto& data = (*user)->custom_data()) { - std::string json = bson::Bson(*data).to_string(); - return duplicate_string(json); - } - - return nullptr; + return with_app_user(user, [&](auto& user) -> char* { + if (const auto& data = user->custom_data()) { + std::string json = bson::Bson(*data).to_string(); + return duplicate_string(json); + } + return nullptr; + }); } RLM_API char* realm_user_get_access_token(const realm_user_t* user) @@ -736,39 +757,35 @@ RLM_API char* realm_user_get_refresh_token(const realm_user_t* user) RLM_API realm_app_t* realm_user_get_app(const realm_user_t* user) noexcept { REALM_ASSERT(user); - try { - if (auto shared_app = (*user)->sync_manager()->app().lock()) { - return new realm_app_t(shared_app); - } - } - catch (const std::exception&) { - } - return nullptr; + return with_app_user(user, [&](auto& user) { + return new realm_app_t(user->app()); + }); } -RLM_API realm_sync_user_subscription_token_t* +RLM_API realm_app_user_subscription_token_t* realm_sync_user_on_state_change_register_callback(realm_user_t* user, realm_sync_on_user_state_changed_t callback, realm_userdata_t userdata, realm_free_userdata_func_t userdata_free) { - return wrap_err([&] { + return with_app_user(user, [&](auto& user) { auto cb = [callback, userdata = SharedUserdata{userdata, FreeUserdata(userdata_free)}](const SyncUser& sync_user) { callback(userdata.get(), realm_user_state_e(sync_user.state())); }; - auto token = (*user)->subscribe(std::move(cb)); - return new realm_sync_user_subscription_token_t{*user, std::move(token)}; + auto token = user->subscribe(std::move(cb)); + return new realm_app_user_subscription_token_t{user, std::move(token)}; }); } +namespace { template -inline util::Optional convert_to_optional(T data) +util::Optional convert_to_optional(T data) { return data ? util::Optional(data) : util::Optional(); } template -inline util::Optional convert_to_optional_bson(realm_string_t doc) +util::Optional convert_to_optional_bson(realm_string_t doc) { if (doc.data == nullptr || doc.size == 0) { return util::Optional(); @@ -777,13 +794,13 @@ inline util::Optional convert_to_optional_bson(realm_string_t doc) } template -inline T convert_to_bson(realm_string_t doc) +T convert_to_bson(realm_string_t doc) { auto res = convert_to_optional_bson(doc); return res ? *res : T(); } -static MongoCollection::FindOptions to_mongodb_collection_find_options(const realm_mongodb_find_options_t* options) +MongoCollection::FindOptions to_mongodb_collection_find_options(const realm_mongodb_find_options_t* options) { MongoCollection::FindOptions mongodb_options; mongodb_options.projection_bson = convert_to_optional_bson(options->projection_bson); @@ -792,7 +809,7 @@ static MongoCollection::FindOptions to_mongodb_collection_find_options(const rea return mongodb_options; } -static MongoCollection::FindOneAndModifyOptions +MongoCollection::FindOneAndModifyOptions to_mongodb_collection_find_one_and_modify_options(const realm_mongodb_find_one_and_modify_options_t* options) { MongoCollection::FindOneAndModifyOptions mongodb_options; @@ -803,8 +820,8 @@ to_mongodb_collection_find_one_and_modify_options(const realm_mongodb_find_one_a return mongodb_options; } -static void handle_mongodb_collection_result(util::Optional bson, util::Optional app_error, - UserdataPtr data, realm_mongodb_callback_t callback) +void handle_mongodb_collection_result(util::Optional bson, util::Optional app_error, + UserdataPtr data, realm_mongodb_callback_t callback) { if (app_error) { auto error = to_capi(*app_error); @@ -815,6 +832,7 @@ static void handle_mongodb_collection_result(util::Optional bson, ut callback(data.get(), {bson_data.c_str(), bson_data.size()}, nullptr); } } +} // anonymous namespace RLM_API realm_mongodb_collection_t* realm_mongo_collection_get(realm_user_t* user, const char* service, const char* database, const char* collection) @@ -823,8 +841,8 @@ RLM_API realm_mongodb_collection_t* realm_mongo_collection_get(realm_user_t* use REALM_ASSERT(service); REALM_ASSERT(database); REALM_ASSERT(collection); - return wrap_err([&]() { - auto col = (*user)->mongo_client(service).db(database).collection(collection); + return with_app_user(user, [&](auto& user) { + auto col = user->mongo_client(service).db(database).collection(collection); return new realm_mongodb_collection_t(col); }); } diff --git a/src/realm/object-store/c_api/notifications.cpp b/src/realm/object-store/c_api/notifications.cpp index 1d06e58b284..dea81306629 100644 --- a/src/realm/object-store/c_api/notifications.cpp +++ b/src/realm/object-store/c_api/notifications.cpp @@ -209,8 +209,6 @@ RLM_API void realm_collection_changes_get_num_changes(const realm_collection_cha size_t* out_num_modifications, size_t* out_num_moves, bool* out_collection_was_cleared) { - // FIXME: This has O(n) performance, which seems ridiculous. - if (out_num_deletions) *out_num_deletions = changes->deletions.count(); if (out_num_insertions) diff --git a/src/realm/object-store/c_api/realm.cpp b/src/realm/object-store/c_api/realm.cpp index edf1d5ad819..d0db803ca57 100644 --- a/src/realm/object-store/c_api/realm.cpp +++ b/src/realm/object-store/c_api/realm.cpp @@ -319,8 +319,6 @@ RLM_API realm_t* realm_from_thread_safe_reference(realm_thread_safe_reference_t* throw LogicError{ErrorCodes::IllegalOperation, "Thread safe reference type mismatch"}; } - // FIXME: This moves out of the ThreadSafeReference, so it isn't - // reusable. std::shared_ptr sch; if (scheduler) { sch = *scheduler; diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 27ae222b61f..1c670847f67 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 { @@ -741,7 +723,7 @@ realm_sync_session_get_connection_state(const realm_sync_session_t* session) noe RLM_API realm_user_t* realm_sync_session_get_user(const realm_sync_session_t* session) noexcept { - return new realm_user_t((*session)->user()); + return new realm_user_t(std::static_pointer_cast((*session)->user())); } RLM_API const char* realm_sync_session_get_partition_value(const realm_sync_session_t* session) noexcept @@ -775,7 +757,7 @@ RLM_API bool realm_sync_immediately_run_file_actions(realm_app_t* realm_app, con bool* did_run) noexcept { return wrap_err([&]() { - *did_run = (*realm_app)->sync_manager()->immediately_run_file_actions(sync_path); + *did_run = (*realm_app)->immediately_run_file_actions(sync_path); return true; }); } diff --git a/src/realm/object-store/c_api/types.hpp b/src/realm/object-store/c_api/types.hpp index 317fc09fec9..6e7c6894521 100644 --- a/src/realm/object-store/c_api/types.hpp +++ b/src/realm/object-store/c_api/types.hpp @@ -1,30 +1,30 @@ #ifndef REALM_OBJECT_STORE_C_API_TYPES_HPP #define REALM_OBJECT_STORE_C_API_TYPES_HPP -#include #include -#include -#include #include -#include -#include +#include +#include #include #include -#include +#include +#include #include +#include #if REALM_ENABLE_SYNC #include +#include #include -#include #include #include #include #include #endif +#include #include #include @@ -570,15 +570,15 @@ struct realm_results : realm::c_api::WrapC, realm::Results { #if REALM_ENABLE_SYNC -struct realm_sync_user_subscription_token : realm::c_api::WrapC { - using Token = realm::Subscribable::Token; - realm_sync_user_subscription_token(std::shared_ptr user, Token&& token) +struct realm_app_user_subscription_token : realm::c_api::WrapC { + using Token = realm::Subscribable::Token; + realm_app_user_subscription_token(std::shared_ptr user, Token&& token) : user(user) , token(std::move(token)) { } - ~realm_sync_user_subscription_token(); - std::shared_ptr user; + ~realm_app_user_subscription_token(); + std::shared_ptr user; Token token; }; @@ -625,8 +625,8 @@ struct realm_http_transport : realm::c_api::WrapC, std::shared_ptr #if REALM_ENABLE_SYNC -#include #include #include #include @@ -133,6 +132,10 @@ void RealmCoordinator::set_config(const Realm::Config& config) throw InvalidArgument(ErrorCodes::IllegalCombination, "Cannot specify a partition value when flexible sync is enabled"); } + if (!config.sync_config->user) { + throw InvalidArgument(ErrorCodes::IllegalCombination, + "A user must be provided to open a synchronized Realm."); + } } #endif @@ -188,7 +191,6 @@ void RealmCoordinator::set_config(const Realm::Config& config) ErrorCodes::MismatchedConfig, util::format("Realm at path '%1' already opened with different sync user.", config.path)); } - if (m_config.sync_config->partition_value != config.sync_config->partition_value) { throw LogicError( ErrorCodes::MismatchedConfig, @@ -433,10 +435,13 @@ bool RealmCoordinator::open_db() #if REALM_ENABLE_SYNC if (m_config.sync_config) { + REALM_ASSERT(m_config.sync_config->user); // If we previously opened this Realm, we may have a lingering sync // session which outlived its RealmCoordinator. If that happens we // want to reuse it instead of creating a new DB. - m_sync_session = m_config.sync_config->user->sync_manager()->get_existing_session(m_config.path); + if (auto sync_manager = m_config.sync_config->user->sync_manager()) { + m_sync_session = sync_manager->get_existing_session(m_config.path); + } if (m_sync_session) { m_db = SyncSession::Internal::get_db(*m_sync_session); init_external_helpers(); @@ -538,8 +543,17 @@ void RealmCoordinator::init_external_helpers() #if REALM_ENABLE_SYNC // We may have reused an existing sync session that outlived its original // RealmCoordinator. If not, we need to create a new one now. - if (m_config.sync_config && !m_sync_session) - m_sync_session = m_config.sync_config->user->sync_manager()->get_session(m_db, m_config); + if (m_config.sync_config && !m_sync_session) { + if (auto sync_manager = m_config.sync_config->user->sync_manager()) { + m_sync_session = sync_manager->get_session(m_db, m_config); + } + else { + 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())); + } + } #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 8ec66878f30..d8f0c3eda69 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 3f4d1cd8855..9564cfeb832 100644 --- a/src/realm/object-store/sync/app.cpp +++ b/src/realm/object-store/sync/app.cpp @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////// -#include "external/json/json.hpp" #include #include @@ -24,7 +23,10 @@ #include #include #include +#include #include +#include +#include #include #include @@ -32,6 +34,7 @@ #include #endif +#include #include #include @@ -142,7 +145,7 @@ enum class RequestTokenType { NoAuth, AccessToken, RefreshToken }; // generate the request headers for a HTTP call, by default it will generate headers with a refresh token if a user is // passed -HttpHeaders get_request_headers(const std::shared_ptr& with_user_authorization = nullptr, +HttpHeaders get_request_headers(const std::shared_ptr& with_user_authorization = nullptr, RequestTokenType token_type = RequestTokenType::RefreshToken) { HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}}; @@ -181,59 +184,30 @@ constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys"; constexpr static int s_max_http_redirects = 20; static util::FlatMap> s_apps_cache; // app_id -> base_url -> app std::mutex s_apps_mutex; - } // anonymous namespace namespace realm { namespace app { - -App::Config::DeviceInfo::DeviceInfo() - : platform(util::get_library_platform()) - , cpu_arch(util::get_library_cpu_arch()) - , core_version(REALM_VERSION_STRING) -{ -} - -App::Config::DeviceInfo::DeviceInfo(std::string a_platform_version, std::string an_sdk_version, std::string an_sdk, - std::string a_device_name, std::string a_device_version, - std::string a_framework_name, std::string a_framework_version, - std::string a_bundle_id) - : DeviceInfo() -{ - platform_version = a_platform_version; - sdk_version = an_sdk_version; - sdk = an_sdk; - device_name = a_device_name; - device_version = a_device_version; - framework_name = a_framework_name; - framework_version = a_framework_version; - bundle_id = a_bundle_id; -} - // NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive. // "Calling function configure requires negative capability '!app->m_route_mutex'" // But 'app' is an object just created in this static method so it is not possible to annotate this in the header. -SharedApp App::get_app(CacheMode mode, const Config& config, - const SyncClientConfig& sync_client_config) NO_THREAD_SAFETY_ANALYSIS +SharedApp App::get_app(CacheMode mode, const AppConfig& config) NO_THREAD_SAFETY_ANALYSIS { if (mode == CacheMode::Enabled) { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(s_default_base_url))]; if (!app) { app = std::make_shared(Private(), config); - app->configure(sync_client_config); } return app; } REALM_ASSERT(mode == CacheMode::Disabled); - auto app = std::make_shared(Private(), config); - app->configure(sync_client_config); - return app; + return std::make_shared(Private(), config); } SharedApp App::get_cached_app(const std::string& app_id, const std::optional& base_url) { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); if (auto it = s_apps_cache.find(app_id); it != s_apps_cache.end()) { const auto& apps_by_url = it->second; @@ -248,13 +222,13 @@ SharedApp App::get_cached_app(const std::string& app_id, const std::optional lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); s_apps_cache.clear(); } void App::close_all_sync_sessions() { - std::lock_guard lock(s_apps_mutex); + std::lock_guard lock(s_apps_mutex); for (auto& apps_by_url : s_apps_cache) { for (auto& app : apps_by_url.second) { app.second->sync_manager()->close_all_sessions(); @@ -262,11 +236,13 @@ void App::close_all_sync_sessions() } } -App::App(Private, const Config& config) +App::App(Private, const AppConfig& config) : m_config(config) , m_base_url(m_config.base_url.value_or(std::string(s_default_base_url))) - , m_location_updated(false) , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms)) + , m_file_manager(std::make_unique(config)) + , 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) { @@ -274,7 +250,6 @@ App::App(Private, const Config& config) } #endif REALM_ASSERT(m_config.transport); - REALM_ASSERT(!m_config.device_info.platform.empty()); // if a base url is provided, then verify the value if (m_config.base_url) { @@ -300,24 +275,13 @@ App::App(Private, const Config& config) App::~App() {} -void App::configure(const SyncClientConfig& sync_client_config) -{ - { - util::CheckedLockGuard guard(m_route_mutex); - // Make sure to request the location when the app is configured - m_location_updated = false; - } - - // Start with an empty sync route in the sync manager. It will ensure the - // location has been updated at least once when the first sync session is - // started by requesting a new access token. - m_sync_manager = SyncManager::create(shared_from_this(), {}, sync_client_config, config().app_id); -} - bool App::init_logger() { - if (!m_logger_ptr && m_sync_manager) { + if (!m_logger_ptr) { m_logger_ptr = m_sync_manager->get_logger(); + if (!m_logger_ptr) { + m_logger_ptr = util::Logger::get_default_logger(); + } } return bool(m_logger_ptr); } @@ -367,7 +331,6 @@ std::string App::get_ws_host_url() return m_ws_host_url; } - std::string App::make_sync_route(Optional ws_host_url) { return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id, @@ -494,7 +457,7 @@ std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path } void App::UserAPIKeyProviderClient::create_api_key( - const std::string& name, const std::shared_ptr& user, + const std::string& name, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -506,7 +469,7 @@ void App::UserAPIKeyProviderClient::create_api_key( UserAPIKeyResponseHandler{std::move(completion)}); } -void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -518,7 +481,7 @@ void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, con } void App::UserAPIKeyProviderClient::fetch_api_keys( - const std::shared_ptr& user, + const std::shared_ptr& user, UniqueFunction&&, Optional)>&& completion) { Request req; @@ -547,7 +510,7 @@ void App::UserAPIKeyProviderClient::fetch_api_keys( }); } -void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -558,7 +521,7 @@ void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, co handle_default_response(std::move(completion))); } -void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -569,7 +532,7 @@ void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, co handle_default_response(std::move(completion))); } -void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, +void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, UniqueFunction)>&& completion) { Request req; @@ -581,14 +544,44 @@ void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, c } // MARK: - App -std::shared_ptr App::current_user() const +std::shared_ptr App::get_user_for_id(const std::string& user_id) +{ + if (auto& user = m_users[user_id]) { + return user->shared_from_this(); + } + 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() { - return m_sync_manager->get_current_user(); + util::CheckedLockGuard lock(m_user_mutex); + if (m_current_user && m_current_user->is_logged_in()) + return m_current_user->shared_from_this(); + if (auto user_id = m_metadata_store->get_current_user(); !user_id.empty()) { + auto user = get_user_for_id(user_id); + m_current_user = user.get(); + return user; + } + return nullptr; } -std::vector> App::all_users() const +std::shared_ptr App::get_existing_logged_in_user(std::string_view user_id) { - return m_sync_manager->all_users(); + util::CheckedLockGuard lock(m_user_mutex); + if (auto it = m_users.find(std::string(user_id)); it != m_users.end() && it->second->is_logged_in()) { + return it->second->shared_from_this(); + } + 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 @@ -629,8 +622,20 @@ void App::update_base_url(std::string_view new_base_url, UniqueFunction& sync_user, - UniqueFunction&, Optional)>&& completion) +std::vector> App::all_users() +{ + util::CheckedLockGuard lock(m_user_mutex); + auto user_ids = m_metadata_store->get_all_users(); + std::vector> users; + users.reserve(user_ids.size()); + for (auto& user_id : user_ids) { + users.push_back(get_user_for_id(user_id)); + } + return users; +} + +void App::get_profile(const std::shared_ptr& user, + UniqueFunction&, Optional)>&& completion) { Request req; req.method = HttpMethod::get; @@ -639,8 +644,9 @@ void App::get_profile(const std::shared_ptr& sync_user, req.uses_refresh_token = false; do_authenticated_request( - std::move(req), sync_user, - [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) { + std::move(req), user, + [completion = std::move(completion), self = shared_from_this(), user, + this](const Response& profile_response) { if (auto error = AppUtils::check_for_errors(profile_response)) { return completion(nullptr, std::move(error)); } @@ -649,24 +655,25 @@ 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, {}); }); } @@ -674,28 +681,27 @@ void App::attach_auth_options(BsonDocument& body) { BsonDocument options; - log_debug("App: version info: platform: %1 version: %2 - sdk: %3 - sdk version: %4 - core version: %5", - m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk, - m_config.device_info.sdk_version, m_config.device_info.core_version); + // log_debug("App: version info: platform: %1 version: %2 - sdk: %3 - sdk version: %4 - core version: %5", + // m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk, + // m_config.device_info.sdk_version, m_config.device_info.core_version); options["appId"] = m_config.app_id; - options["platform"] = m_config.device_info.platform; + options["platform"] = util::get_library_platform(); options["platformVersion"] = m_config.device_info.platform_version; options["sdk"] = m_config.device_info.sdk; options["sdkVersion"] = m_config.device_info.sdk_version; - options["cpuArch"] = m_config.device_info.cpu_arch; + options["cpuArch"] = util::get_library_cpu_arch(); options["deviceName"] = m_config.device_info.device_name; options["deviceVersion"] = m_config.device_info.device_version; options["frameworkName"] = m_config.device_info.framework_name; options["frameworkVersion"] = m_config.device_info.framework_version; - options["coreVersion"] = m_config.device_info.core_version; + options["coreVersion"] = REALM_VERSION_STRING; options["bundleId"] = m_config.device_info.bundle_id; body["options"] = BsonDocument({{"device", options}}); } -void App::log_in_with_credentials( - const AppCredentials& credentials, const std::shared_ptr& linking_user, - UniqueFunction&, Optional)>&& completion) +void App::log_in_with_credentials(const AppCredentials& credentials, const std::shared_ptr& linking_user, + UniqueFunction&, Optional)>&& completion) { if (would_log(util::Logger::Level::debug)) { auto app_info = util::format("app_id: %1", m_config.app_id); @@ -703,15 +709,32 @@ void App::log_in_with_credentials( } // if we try logging in with an anonymous user while there // is already an anonymous session active, reuse it + std::shared_ptr anon_user; if (credentials.provider() == AuthProvider::ANONYMOUS) { - for (auto&& user : m_sync_manager->all_users()) { + util::CheckedLockGuard lock(m_user_mutex); + for (auto& [_, user] : m_users) { if (user->is_anonymous()) { - completion(switch_user(user), util::none); - return; + anon_user = user->shared_from_this(); + m_current_user = user; + m_metadata_store->set_current_user(user->user_id()); + break; } } } + if (anon_user) { + emit_change_to_subscribers(*this); + completion(anon_user, util::none); + return; + } + + if (linking_user) { + util::CheckedLockGuard lock(m_user_mutex); + if (!verify_user_present(linking_user)) { + return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found.")); + } + } + // construct the route std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(), linking_user ? "?link=true" : ""); @@ -722,60 +745,75 @@ void App::log_in_with_credentials( do_request( {HttpMethod::post, route, m_request_timeout_ms, get_request_headers(linking_user, RequestTokenType::AccessToken), Bson(body).to_string()}, - [completion = std::move(completion), credentials, linking_user, - self = shared_from_this()](const Response& response) mutable { + [completion = std::move(completion), credentials, linking_user, self = shared_from_this(), + this](const Response& response) mutable { if (auto error = AppUtils::check_for_errors(response)) { - self->log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code, - error->what()); + log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code, + error->what()); return completion(nullptr, std::move(error)); } - std::shared_ptr sync_user = linking_user; + std::shared_ptr sync_user = linking_user; try { auto json = parse(response.body); if (linking_user) { - linking_user->update_access_token(get(json, "access_token")); + if (auto user_data = m_metadata_store->get_user(linking_user->user_id())) { + user_data->access_token = RealmJWT(get(json, "access_token")); + // maybe a callback for this? + m_metadata_store->update_user(linking_user->user_id(), *user_data); + linking_user->update_backing_data(std::move(user_data)); + } } else { - sync_user = self->m_sync_manager->get_user( - get(json, "user_id"), get(json, "refresh_token"), - get(json, "access_token"), get(json, "device_id")); + auto user_id = get(json, "user_id"); + m_metadata_store->create_user(user_id, get(json, "refresh_token"), + get(json, "access_token"), + get(json, "device_id")); + util::CheckedLockGuard lock(m_user_mutex); + user_data_updated(user_id); // FIXME: needs to be callback from metadata store + sync_user = get_user_for_id(user_id); } } catch (const AppError& e) { return completion(nullptr, e); } - - self->get_profile(sync_user, std::move(completion)); + // If the user has not been logged in, then there is a problem with the token + if (!sync_user->is_logged_in()) { + return completion(nullptr, + AppError(ErrorCodes::BadToken, "Could not log in user: received malformed JWT")); + } + switch_user(sync_user); + get_profile(sync_user, std::move(completion)); }, false); } void App::log_in_with_credentials( const AppCredentials& credentials, - util::UniqueFunction&, Optional)>&& completion) + util::UniqueFunction&, Optional)>&& completion) { App::log_in_with_credentials(credentials, nullptr, std::move(completion)); } -void App::log_out(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::log_out(const std::shared_ptr& user, SyncUser::State new_state, + UniqueFunction)>&& completion) { - if (!user || user->state() != SyncUser::State::LoggedIn) { - log_debug("App: log_out() - already logged out"); - return completion(util::none); + if (!user || user->state() == new_state || user->state() == SyncUser::State::Removed) { + if (completion) { + completion(util::none); + } + return; } - log_debug("App: log_out(%1)", user->user_profile().name()); - auto refresh_token = user->refresh_token(); - user->log_out(); - Request req; req.method = HttpMethod::del; req.url = url_for_path("/auth/session"); req.timeout_ms = m_request_timeout_ms; req.uses_refresh_token = true; - req.headers = get_request_headers(); - req.headers.insert({"Authorization", util::format("Bearer %1", refresh_token)}); + req.headers = get_request_headers(user); + + m_metadata_store->log_out(user->user_id(), new_state); + user->update_backing_data(m_metadata_store->get_user(user->user_id())); do_request(std::move(req), [self = shared_from_this(), completion = std::move(completion)](const Response& response) { @@ -783,61 +821,80 @@ void App::log_out(const std::shared_ptr& user, UniqueFunctionemit_change_to_subscribers(*self); } - completion(error); + if (completion) { + completion(error); + } }); } +void App::log_out(const std::shared_ptr& user, UniqueFunction)>&& completion) +{ + auto new_state = user && user->is_anonymous() ? SyncUser::State::Removed : SyncUser::State::LoggedOut; + log_out(user, new_state, std::move(completion)); +} + void App::log_out(UniqueFunction)>&& completion) { - log_debug("App: log_out(current user)"); - log_out(m_sync_manager->get_current_user(), std::move(completion)); + log_out(current_user(), std::move(completion)); } -bool App::verify_user_present(const std::shared_ptr& user) const +bool App::verify_user_present(const std::shared_ptr& user) const { - auto users = m_sync_manager->all_users(); - return std::any_of(users.begin(), users.end(), [&](auto&& u) { - return u == user; - }); + for (auto& [_, u] : m_users) { + if (u == user.get()) + return true; + } + return false; } -std::shared_ptr App::switch_user(const std::shared_ptr& user) const +void App::switch_user(const std::shared_ptr& user) { if (!user || user->state() != SyncUser::State::LoggedIn) { throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out"); } + util::CheckedLockGuard lock(m_user_mutex); if (!verify_user_present(user)) { throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist"); } - m_sync_manager->set_current_user(user->identity()); + m_current_user = user.get(); + m_metadata_store->set_current_user(user->user_id()); emit_change_to_subscribers(*this); - return m_sync_manager->get_current_user(); } -void App::remove_user(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::remove_user(const std::shared_ptr& user, UniqueFunction)>&& completion) { if (!user || user->state() == SyncUser::State::Removed) { return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed")); } - if (!verify_user_present(user)) { - return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found")); + + { + util::CheckedLockGuard lock(m_user_mutex); + if (!verify_user_present(user)) { + return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found")); + } } if (user->is_logged_in()) { - log_out(user, [user, completion = std::move(completion), - self = shared_from_this()](const Optional& error) { - self->m_sync_manager->remove_user(user->identity()); - return completion(error); - }); + log_out( + user, SyncUser::State::Removed, + [user, completion = std::move(completion), self = shared_from_this()](const Optional& error) { + user->update_backing_data(std::nullopt); + if (completion) { + completion(error); + } + }); } else { - m_sync_manager->remove_user(user->identity()); - return completion({}); + m_metadata_store->log_out(user->user_id(), SyncUser::State::Removed); + user->update_backing_data(std::nullopt); + if (completion) { + completion(std::nullopt); + } } } -void App::delete_user(const std::shared_ptr& user, UniqueFunction)>&& completion) +void App::delete_user(const std::shared_ptr& user, UniqueFunction)>&& completion) { if (!user) { return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found.")); @@ -846,28 +903,33 @@ void App::delete_user(const std::shared_ptr& user, UniqueFunctionidentity()](const Response& response) { - auto error = AppUtils::check_for_errors(response); - if (!error) { - self->emit_change_to_subscribers(*self); - self->m_sync_manager->delete_user(identity); - } - completion(std::move(error)); - }); + do_authenticated_request( + std::move(req), user, + [self = shared_from_this(), completion = std::move(completion), user, this](const Response& response) { + auto error = AppUtils::check_for_errors(response); + if (!error) { + auto user_id = user->user_id(); + user->detach_and_tear_down(); + m_metadata_store->delete_user(*m_file_manager, user_id); + emit_change_to_subscribers(*self); + } + completion(std::move(error)); + }); } -void App::link_user(const std::shared_ptr& user, const AppCredentials& credentials, - UniqueFunction&, Optional)>&& completion) +void App::link_user(const std::shared_ptr& user, const AppCredentials& credentials, + UniqueFunction&, Optional)>&& completion) { if (!user) { return completion(nullptr, @@ -877,20 +939,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)); @@ -907,9 +970,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, @@ -1001,13 +1062,10 @@ std::optional App::update_location(const Response& response, const std // a valid response. base_url is the new hostname or m_base_url value when request_location() // was called. - // Check for errors in the response if (auto error = AppUtils::check_for_errors(response)) { return error; } - REALM_ASSERT(m_sync_manager); // Need a valid sync manager - // Update the location info with the data from the response try { auto json = parse(response.body); @@ -1126,7 +1184,7 @@ void App::check_for_redirect_response(Request&& request, const Response& respons update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location)); } -void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, +void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, util::UniqueFunction&& completion) { request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken @@ -1147,7 +1205,7 @@ void App::do_authenticated_request(Request&& request, const std::shared_ptr& sync_user, + const std::shared_ptr& sync_user, util::UniqueFunction&& completion) { // Only handle auth failures @@ -1165,25 +1223,23 @@ void App::handle_auth_failure(const AppError& error, const Response& response, R return; } - // Otherwise, refresh the access token - App::refresh_access_token(sync_user, false, - [self = shared_from_this(), request = std::move(request), - completion = std::move(completion), response = std::move(response), - sync_user](Optional&& error) mutable { - if (!error) { - // assign the new access_token to the auth header - request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken); - self->do_request(std::move(request), std::move(completion)); - } - else { - // pass the error back up the chain - completion(std::move(response)); - } - }); + refresh_access_token(sync_user, false, + [self = shared_from_this(), request = std::move(request), completion = std::move(completion), + response = std::move(response), sync_user](Optional&& error) mutable { + if (!error) { + // assign the new access_token to the auth header + request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken); + self->do_request(std::move(request), std::move(completion)); + } + else { + // pass the error back up the chain + completion(std::move(response)); + } + }); } /// MARK: - refresh access token -void App::refresh_access_token(const std::shared_ptr& sync_user, bool update_location, +void App::refresh_access_token(const std::shared_ptr& sync_user, bool update_location, util::UniqueFunction)>&& completion) { if (!sync_user) { @@ -1203,14 +1259,19 @@ void App::refresh_access_token(const std::shared_ptr& sync_user, bool do_request( {HttpMethod::post, url_for_path("/auth/session"), m_request_timeout_ms, get_request_headers(sync_user, RequestTokenType::RefreshToken)}, - [completion = std::move(completion), sync_user](const Response& response) { + [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& response) { if (auto error = AppUtils::check_for_errors(response)) { return completion(std::move(error)); } try { auto json = parse(response.body); - sync_user->update_access_token(get(json, "access_token")); + RealmJWT access_token{get(json, "access_token")}; + if (auto data = self->m_metadata_store->get_user(sync_user->user_id())) { + data->access_token = access_token; + self->m_metadata_store->update_user(sync_user->user_id(), *data); + sync_user->update_backing_data(std::move(data)); + } } catch (AppError& err) { return completion(std::move(err)); @@ -1227,7 +1288,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) { @@ -1252,7 +1313,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) { @@ -1292,7 +1353,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)); @@ -1302,16 +1363,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{ @@ -1342,7 +1403,38 @@ Request App::make_streaming_request(const std::shared_ptr& user, const PushClient App::push_notification_client(const std::string& service_name) { - return PushClient(service_name, m_config.app_id, m_request_timeout_ms, shared_from_this()); + return PushClient(service_name, m_config.app_id, m_request_timeout_ms, + std::shared_ptr(shared_from_this(), this)); +} + +// MARK: - UserProvider + +void App::register_sync_user(User& user) +{ + auto& tracked_user = m_users[user.user_id()]; + REALM_ASSERT(!tracked_user); + tracked_user = &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); + REALM_ASSERT(m_users.count(user.user_id())); + m_users.erase(user.user_id()); + if (m_current_user == &user) { + m_current_user = nullptr; + } +} + +bool App::immediately_run_file_actions(std::string_view realm_path) +{ + return m_metadata_store->immediately_run_file_actions(*m_file_manager, realm_path); +} + +std::string App::path_for_realm(const SyncConfig& config, std::optional custom_file_name) const +{ + return m_file_manager->path_for_realm(config, std::move(custom_file_name)); } } // namespace app diff --git a/src/realm/object-store/sync/app.hpp b/src/realm/object-store/sync/app.hpp index c5cf5c4a15a..bfe2661bf27 100644 --- a/src/realm/object-store/sync/app.hpp +++ b/src/realm/object-store/sync/app.hpp @@ -19,30 +19,29 @@ #ifndef REALM_APP_HPP #define REALM_APP_HPP +#include #include #include #include -#include #include #include +#include #include +#include #include -#include -#include #include -#include - namespace realm { -class SyncUser; class SyncSession; class SyncManager; -struct SyncClientConfig; +class SyncFileManager; namespace app { class App; +class MetadataStore; +class User; typedef std::shared_ptr SharedApp; @@ -52,67 +51,133 @@ typedef std::shared_ptr SharedApp; /// /// You can also use it to execute [Functions](https://docs.mongodb.com/stitch/functions/). class App : public std::enable_shared_from_this, - public AuthRequestClient, - public AppServiceClient, + private AuthRequestClient, + private AppServiceClient, public Subscribable { - struct Private {}; public: - struct Config { - // Information about the device where the app is running - struct DeviceInfo { - std::string platform_version; // json: platformVersion - std::string sdk_version; // json: sdkVersion - std::string sdk; // json: sdk - std::string device_name; // json: deviceName - std::string device_version; // json: deviceVersion - std::string framework_name; // json: frameworkName - std::string framework_version; // json: frameworkVersion - std::string bundle_id; // json: bundleId - - DeviceInfo(); - DeviceInfo(std::string, std::string, std::string, std::string, std::string, std::string, std::string, - std::string); - - private: - friend App; - - std::string platform; // json: platform - std::string cpu_arch; // json: cpuArch - std::string core_version; // json: coreVersion - }; - - std::string app_id; - std::shared_ptr transport; - util::Optional base_url; - util::Optional default_request_timeout_ms; - DeviceInfo device_info; + // MARK: - App Initialization + enum class CacheMode { + Enabled, // Return a cached app instance if one was previously generated for `config`'s app_id+base_url combo, + Disabled // Bypass the app cache; return a new app instance. }; + /// Get a shared pointer to a configured App instance. Sync is fully enabled and the external backing store + /// factory provided is used to create a store if the cache is not used. If you want the + /// default storage engine, construct a RealmMetadataStore instance in the factory. + static SharedApp get_app(CacheMode mode, const AppConfig& config); + + /// Return a cached app instance if one was previously generated for the `app_id`+`base_url` combo using + /// `App::get_app()`. + /// If base_url is not provided, and there are multiple cached apps with the same app_id but different base_urls, + /// then a non-determinstic one will be returned. + /// + /// Prefer using `App::get_app()` or populating `base_url` to avoid the non-deterministic behavior. + static SharedApp get_cached_app(const std::string& app_id, + const std::optional& base_url = std::nullopt); + + /// Clear the cache used for `get_app(CacheMode::Enable)` and `get_cached_app()`. + static void clear_cached_apps(); - // `enable_shared_from_this` is unsafe with public constructors; - // use `App::get_app()` instead - explicit App(Private, const Config& config); + explicit App(Private, const AppConfig& config); App(App&&) noexcept = delete; App& operator=(App&&) noexcept = delete; ~App(); - const Config& config() const + const AppConfig& config() const { return m_config; } - /// Get the last used user. - std::shared_ptr current_user() const; + // MARK: - Other objects owned by App + const std::shared_ptr& sync_manager() const + { + return m_sync_manager; + } - /// Get all users. - std::vector> all_users() const; + std::shared_ptr auth_request_client() + { + return std::shared_ptr(shared_from_this(), this); + } - std::shared_ptr const& sync_manager() const + std::shared_ptr app_service_client() { - return m_sync_manager; + return std::shared_ptr(shared_from_this(), this); } + // MARK: - User Management + + /// Get the last used user. + std::shared_ptr current_user() REQUIRES(!m_user_mutex); + /// Get the user object for the given `user_id` if a user with that id is logged in, or nullptr if not. + std::shared_ptr get_existing_logged_in_user(std::string_view user_id) REQUIRES(!m_user_mutex); + /// Get all users. + std::vector> all_users() REQUIRES(!m_user_mutex); + /// Set the current user to the given one. The user must be logged in and have been obtained from this `App` + /// instance. + void switch_user(const std::shared_ptr& user) REQUIRES(!m_user_mutex); + + /// Log in a user and asynchronously retrieve a user object. + /// If the log in completes successfully, the completion block will be called, and a + /// `User` representing the logged-in user will be passed to it. This user object + /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the + /// completion block will be called with an error. + /// + /// @param credentials A `SyncCredentials` object representing the user to log in. + /// @param completion A callback block to be invoked once the log in completes. + void log_in_with_credentials( + const AppCredentials& credentials, + util::UniqueFunction&, std::optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Logout the current user. + void log_out(util::UniqueFunction)>&&) REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Refreshes the custom data for a specified user + /// @param user The user you want to refresh + /// @param update_location If true, the location metadata will be updated before refresh + void refresh_custom_data(const std::shared_ptr& user, bool update_location, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + void refresh_custom_data(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + + /// Log out the given user if they are not already logged out. + void log_out(const std::shared_ptr& user, util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex); + + /// Links the currently authenticated user with a new identity, where the identity is defined by the credential + /// specified as a parameter. This will only be successful if this `User` is the currently authenticated + /// with the client from which it was created. On success the user will be returned with the new identity. + /// + /// @param user The user which will have the credentials linked to, the user must be logged in + /// @param credentials The `AppCredentials` used to link the user to a new identity. + /// @param completion The completion handler to call when the linking is complete. + /// If the operation is successful, the result will contain the original + /// `User` object representing the user. + void link_user(const std::shared_ptr& user, const AppCredentials& credentials, + util::UniqueFunction&, std::optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + + /// Logs out and removes the provided user. + /// This invokes logout on the server. + /// @param user the user to remove + /// @param completion Will return an error if the user is not found or the http request failed. + void remove_user(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + /// Deletes a user and all its data from the server. + /// @param user The user to delete + /// @param completion Will return an error if the user is not found or the http request failed. + void delete_user(const std::shared_ptr& user, + util::UniqueFunction)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); + + // MARK: - Provider Clients + /// A struct representing a user API key as returned by the App server. struct UserAPIKey { // The ID of the key. @@ -120,7 +185,7 @@ class App : public std::enable_shared_from_this, /// The actual key. Will only be included in /// the response when an API key is first created. - util::Optional key; + std::optional key; /// The name of the key. std::string name; @@ -137,41 +202,41 @@ class App : public std::enable_shared_from_this, /// Creates a user API key that can be used to authenticate as the current user. /// @param name The name of the API key to be created. /// @param completion A callback to be invoked once the call is complete. - void create_api_key(const std::string& name, const std::shared_ptr& user, - util::UniqueFunction)>&& completion); + void create_api_key(const std::string& name, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); /// Fetches a user API key associated with the current user. /// @param id The id of the API key to fetch. /// @param completion A callback to be invoked once the call is complete. - void fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, - util::UniqueFunction)>&& completion); + void fetch_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); /// Fetches the user API keys associated with the current user. /// @param completion A callback to be invoked once the call is complete. void - fetch_api_keys(const std::shared_ptr& user, - util::UniqueFunction&&, util::Optional)>&& completion); + fetch_api_keys(const std::shared_ptr& user, + util::UniqueFunction&&, std::optional)>&& completion); /// Deletes a user API key associated with the current user. /// @param id The id of the API key to delete. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, - util::UniqueFunction)>&& completion); + void delete_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); /// Enables a user API key associated with the current user. /// @param id The id of the API key to enable. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, - util::UniqueFunction)>&& completion); + void enable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); /// Disables a user API key associated with the current user. /// @param id The id of the API key to disable. /// @param user The user to perform this operation. /// @param completion A callback to be invoked once the call is complete. - void disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, - util::UniqueFunction)>&& completion); + void disable_api_key(const realm::ObjectId& id, const std::shared_ptr& user, + util::UniqueFunction)>&& completion); private: friend class App; @@ -196,30 +261,30 @@ class App : public std::enable_shared_from_this, /// @param password The password that the user created for the new username/password identity. /// @param completion A callback to be invoked once the call is complete. void register_email(const std::string& email, const std::string& password, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Confirms an email identity with the username/password provider. /// @param token The confirmation token that was emailed to the user. /// @param token_id The confirmation token id that was emailed to the user. /// @param completion A callback to be invoked once the call is complete. void confirm_user(const std::string& token, const std::string& token_id, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Re-sends a confirmation email to a user that has registered but /// not yet confirmed their email address. /// @param email The email address of the user to re-send a confirmation for. /// @param completion A callback to be invoked once the call is complete. void resend_confirmation_email(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); void send_reset_password_email(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Retries the custom confirmation function on a user for a given email. /// @param email The email address of the user to retry the custom confirmation for. /// @param completion A callback to be invoked once the retry is complete. void retry_custom_confirmation(const std::string& email, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Resets the password of an email identity using the /// password reset token emailed to a user. @@ -228,7 +293,7 @@ class App : public std::enable_shared_from_this, /// @param token_id The password reset token id that was emailed to the user. /// @param completion A callback to be invoked once the call is complete. void reset_password(const std::string& password, const std::string& token, const std::string& token_id, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); /// Resets the password of an email identity using the /// password reset function set up in the application. @@ -238,7 +303,7 @@ class App : public std::enable_shared_from_this, /// @param completion A callback to be invoked once the call is complete. void call_reset_password_function(const std::string& email, const std::string& password, const bson::BsonArray& args, - util::UniqueFunction)>&& completion); + util::UniqueFunction)>&& completion); private: friend class App; @@ -250,21 +315,18 @@ class App : public std::enable_shared_from_this, SharedApp m_parent; }; - enum class CacheMode { - Enabled, // Return a cached app instance if one was previously generated for `config`'s app_id+base_url combo, - Disabled // Bypass the app cache; return a new app instance. - }; - /// Get a shared pointer to a configured App instance. - static SharedApp get_app(CacheMode mode, const Config& config, const SyncClientConfig& sync_client_config); - /// Return a cached app instance if one was previously generated for the `app_id`+`base_url` combo using - /// `App::get_app()`. - /// If base_url is not provided, and there are multiple cached apps with the same app_id but different base_urls, - /// then a non-determinstic one will be returned. - /// - /// Prefer using `App::get_app()` or populating `base_url` to avoid the non-deterministic behavior. - static SharedApp get_cached_app(const std::string& app_id, - const std::optional& base_url = std::nullopt); + // Get a provider client for the given class type. + template + T provider_client() + { + return T(this); + } + + // MARK: - App Services + + // Return the base url path used for HTTP AppServices requests + std::string get_host_url() REQUIRES(!m_route_mutex); /// Get the current base URL for the AppServices server used for http requests and sync /// connections. @@ -285,114 +347,41 @@ class App : public std::enable_shared_from_this, void update_base_url(std::string_view base_url, util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - /// Log in a user and asynchronously retrieve a user object. - /// If the log in completes successfully, the completion block will be called, and a - /// `SyncUser` representing the logged-in user will be passed to it. This user object - /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the - /// completion block will be called with an error. - /// - /// @param credentials A `SyncCredentials` object representing the user to log in. - /// @param completion A callback block to be invoked once the log in completes. - void log_in_with_credentials( - const AppCredentials& credentials, - util::UniqueFunction&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex); - - /// Logout the current user. - void log_out(util::UniqueFunction)>&&) REQUIRES(!m_route_mutex); - - /// Refreshes the custom data for a specified user - /// @param user The user you want to refresh - /// @param update_location If true, the location metadata will be updated before refresh - void refresh_custom_data(const std::shared_ptr& user, bool update_location, - util::UniqueFunction)>&& completion) - REQUIRES(!m_route_mutex); - void refresh_custom_data(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) + void call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, + const std::optional& service_name, + util::UniqueFunction)>&& completion) final REQUIRES(!m_route_mutex); - /// Log out the given user if they are not already logged out. - void log_out(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - /// Links the currently authenticated user with a new identity, where the identity is defined by the credential - /// specified as a parameter. This will only be successful if this `SyncUser` is the currently authenticated - /// with the client from which it was created. On success the user will be returned with the new identity. - /// - /// @param user The user which will have the credentials linked to, the user must be logged in - /// @param credentials The `AppCredentials` used to link the user to a new identity. - /// @param completion The completion handler to call when the linking is complete. - /// If the operation is successful, the result will contain the original - /// `SyncUser` object representing the user. void - link_user(const std::shared_ptr& user, const AppCredentials& credentials, - util::UniqueFunction&, util::Optional)>&& completion) + call_function(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::optional& service_name, + util::UniqueFunction&&, std::optional)>&& completion) final REQUIRES(!m_route_mutex); - /// Switches the active user with the specified one. The user must - /// exist in the list of all users who have logged into this application, and - /// the user must be currently logged in, otherwise this will throw an - /// AppError. - /// - /// @param user The user to switch to - /// @returns A shared pointer to the new current user - std::shared_ptr switch_user(const std::shared_ptr& user) const; - - /// Logs out and removes the provided user. - /// This invokes logout on the server. - /// @param user the user to remove - /// @param completion Will return an error if the user is not found or the http request failed. - void remove_user(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - /// Deletes a user and all its data from the server. - /// @param user The user to delete - /// @param completion Will return an error if the user is not found or the http request failed. - void delete_user(const std::shared_ptr& user, - util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); - - // Get a provider client for the given class type. - template - T provider_client() - { - return T(this); - } - - void call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, - const util::Optional& service_name, - util::UniqueFunction)>&& completion) final - REQUIRES(!m_route_mutex); - - void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, - const util::Optional& service_name, - util::UniqueFunction&&, util::Optional)>&& completion) final - REQUIRES(!m_route_mutex); - - void call_function( - const std::shared_ptr& user, const std::string&, const bson::BsonArray& args_bson, - util::UniqueFunction&&, util::Optional)>&& completion) final + void + call_function(const std::shared_ptr& user, const std::string&, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::optional)>&& completion) final REQUIRES(!m_route_mutex); - void call_function( - const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name, - util::UniqueFunction&&, util::Optional)>&& completion) final - REQUIRES(!m_route_mutex); + void + call_function(const std::string& name, const bson::BsonArray& args_bson, + const std::optional& service_name, + util::UniqueFunction&&, std::optional)>&& completion) final + REQUIRES(!m_route_mutex, !m_user_mutex); - void call_function( - const std::string&, const bson::BsonArray& args_bson, - util::UniqueFunction&&, util::Optional)>&& completion) final - REQUIRES(!m_route_mutex); + void + call_function(const std::string&, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::optional)>&& completion) final + REQUIRES(!m_route_mutex, !m_user_mutex); template - void call_function(const std::shared_ptr& user, const std::string& name, - const bson::BsonArray& args_bson, - util::UniqueFunction&&, util::Optional)>&& completion) + void call_function(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + util::UniqueFunction&&, std::optional)>&& completion) REQUIRES(!m_route_mutex) { call_function( user, name, args_bson, util::none, - [completion = std::move(completion)](util::Optional&& value, util::Optional error) { + [completion = std::move(completion)](std::optional&& value, std::optional error) { if (value) { return completion(util::some(static_cast(*value)), std::move(error)); } @@ -403,8 +392,8 @@ class App : public std::enable_shared_from_this, template void call_function(const std::string& name, const bson::BsonArray& args_bson, - util::UniqueFunction&&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex) + util::UniqueFunction&&, std::optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex) { call_function(current_user(), name, args_bson, std::move(completion)); @@ -412,31 +401,37 @@ class App : public std::enable_shared_from_this, // NOTE: only sets "Accept: text/event-stream" header. If you use an API that sets that but doesn't support // setting other headers (eg. EventSource() in JS), you can ignore the headers field on the request. - Request make_streaming_request(const std::shared_ptr& user, const std::string& name, + Request make_streaming_request(const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, - const util::Optional& service_name) const REQUIRES(!m_route_mutex); + const std::optional& service_name) const REQUIRES(!m_route_mutex); - // MARK: Push notification client PushClient push_notification_client(const std::string& service_name); - static void clear_cached_apps(); + // MARK: - Sync // Immediately close all open sync sessions for all cached apps. // Used by JS SDK to ensure no sync clients remain open when a developer // reloads an app (#5411). static void close_all_sync_sessions(); - // Return the base url path used for HTTP AppServices requests - std::string get_host_url() REQUIRES(!m_route_mutex); - // Return the base url path used for Sync Session Websocket requests std::string get_ws_host_url() REQUIRES(!m_route_mutex); + // Get the default path for a Realm for the given configuration. + // The default value is `///.realm`. + // If the file cannot be created at this location, for example due to path length restrictions, + // this function may pass back `/.realm` + std::string path_for_realm(const SyncConfig& config, + std::optional custom_file_name = std::nullopt) const; + + // Attempt to perform all pending file actions for the given path. Returns + // true if any were performed. + bool immediately_run_file_actions(std::string_view realm_path); + private: - const Config m_config; + const AppConfig m_config; util::CheckedMutex m_route_mutex; - // The following variables hold the different paths to Atlas, depending on the // request being performed // Base hostname from config.base_url or update_base_url() for querying location info @@ -454,14 +449,16 @@ class App : public std::enable_shared_from_this, // (e.g. "https://us-east-1.aws.realm.mongodb.com/api/client/v2.0/app//auth") std::string m_auth_route GUARDED_BY(m_route_mutex); // If false, the location info will be updated upon the next AppServices request - bool m_location_updated GUARDED_BY(m_route_mutex); + bool m_location_updated GUARDED_BY(m_route_mutex) = false; // Storage for the location info returned by the base URL location endpoint // Base hostname for AppServices HTTP requests std::string m_host_url GUARDED_BY(m_route_mutex); // Base hostname for Device Sync websocket requests std::string m_ws_host_url GUARDED_BY(m_route_mutex); - uint64_t m_request_timeout_ms; + const uint64_t m_request_timeout_ms; + std::unique_ptr m_file_manager; + std::unique_ptr m_metadata_store; std::shared_ptr m_sync_manager; std::shared_ptr m_logger_ptr; @@ -477,11 +474,11 @@ class App : public std::enable_shared_from_this, template void log_error(const char* message, Params&&... params); - /// Refreshes the access token for a specified `SyncUser` + /// Refreshes the access token for a specified `User` /// @param completion Passes an error should one occur. /// @param update_location If true, the location metadata will be updated before refresh - void refresh_access_token(const std::shared_ptr& user, bool update_location, - util::UniqueFunction)>&& completion) + void refresh_access_token(const std::shared_ptr& user, bool update_location, + util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); /// Checks if an auth failure has taken place and if so it will attempt to refresh the @@ -492,7 +489,7 @@ class App : public std::enable_shared_from_this, /// @param completion returns the original response in the case it is not an auth error, or if a failure /// occurs, if the refresh was a success the newly attempted response will be passed back void handle_auth_failure(const AppError& error, const Response& response, Request&& request, - const std::shared_ptr& sync_user, + const std::shared_ptr& user, util::UniqueFunction&& completion) REQUIRES(!m_route_mutex); std::string url_for_path(const std::string& path) const override REQUIRES(!m_route_mutex); @@ -500,7 +497,7 @@ class App : public std::enable_shared_from_this, /// Return the app route for this App instance, or creates a new app route string if /// a new hostname is provided /// @param hostname The hostname to generate a new app route - std::string get_app_route(const util::Optional& hostname = util::none) const REQUIRES(m_route_mutex); + std::string get_app_route(const std::optional& hostname = util::none) const REQUIRES(m_route_mutex); /// Request the app metadata information from the server if it has not been processed yet. If /// a new hostname is provided, the app metadata will be refreshed using the new hostname. @@ -508,7 +505,7 @@ class App : public std::enable_shared_from_this, /// @param new_hostname The (Original) new hostname to request the location from /// @param redir_location The location provided by the last redirect response when querying location /// @param redirect_count The current number of redirects that have occurred in a row - void request_location(util::UniqueFunction)>&& completion, + void request_location(util::UniqueFunction)>&& completion, std::optional&& new_hostname = std::nullopt, std::optional&& redir_location = std::nullopt, int redirect_count = 0) REQUIRES(!m_route_mutex); @@ -525,9 +522,9 @@ class App : public std::enable_shared_from_this, /// @param completion The original completion object that will be called with the response to the request /// @param new_hostname If provided, the metadata will be requested from this hostname void update_location_and_resend(Request&& request, util::UniqueFunction&& completion, - util::Optional&& new_hostname = util::none) REQUIRES(!m_route_mutex); + std::optional&& new_hostname = util::none) REQUIRES(!m_route_mutex); - void post(std::string&& route, util::UniqueFunction)>&& completion, + void post(std::string&& route, util::UniqueFunction)>&& completion, const bson::BsonDocument& body) REQUIRES(!m_route_mutex); /// Performs a request to the Stitch server. This request does not contain authentication state. @@ -548,38 +545,41 @@ class App : public std::enable_shared_from_this, /// Performs an authenticated request to the Stitch server, using the current authentication state /// @param request The request to be performed /// @param completion Returns the response from the server - void do_authenticated_request(Request&& request, const std::shared_ptr& user, + void do_authenticated_request(Request&& request, const std::shared_ptr& user, util::UniqueFunction&& completion) override REQUIRES(!m_route_mutex); - /// Gets the social profile for a `SyncUser` - /// @param completion Callback will pass the `SyncUser` with the social profile details - void - get_profile(const std::shared_ptr& user, - util::UniqueFunction&, util::Optional)>&& completion) + /// Gets the social profile for a `User`. + /// + /// @param completion Callback will pass the `User` with the social profile details + void get_profile(const std::shared_ptr& user, + util::UniqueFunction&, std::optional)>&& completion) REQUIRES(!m_route_mutex); /// Log in a user and asynchronously retrieve a user object. /// If the log in completes successfully, the completion block will be called, and a - /// `SyncUser` representing the logged-in user will be passed to it. This user object + /// `User` representing the logged-in user will be passed to it. This user object /// can be used to open `Realm`s and retrieve `SyncSession`s. Otherwise, the /// completion block will be called with an error. /// /// @param credentials A `SyncCredentials` object representing the user to log in. - /// @param linking_user A `SyncUser` you want to link these credentials too + /// @param linking_user A `User` you want to link these credentials too /// @param completion A callback block to be invoked once the log in completes. void log_in_with_credentials( - const AppCredentials& credentials, const std::shared_ptr& linking_user, - util::UniqueFunction&, util::Optional)>&& completion) - REQUIRES(!m_route_mutex); + const AppCredentials& credentials, const std::shared_ptr& linking_user, + util::UniqueFunction&, std::optional)>&& completion) + REQUIRES(!m_route_mutex, !m_user_mutex); /// Provides MongoDB Realm Cloud with metadata related to the users session void attach_auth_options(bson::BsonDocument& body); std::string function_call_url_path() const REQUIRES(!m_route_mutex); - void configure(const SyncClientConfig& sync_client_config) REQUIRES(!m_route_mutex); + static SharedApp do_get_app(CacheMode mode, const AppConfig& config, + util::FunctionRef do_config); + + void configure_backing_store(std::unique_ptr store) REQUIRES(!m_route_mutex); std::string make_sync_route(util::Optional ws_host_url = util::none) REQUIRES(m_route_mutex); void configure_route(const std::string& host_url, const std::string& ws_host_url) REQUIRES(m_route_mutex); @@ -588,7 +588,23 @@ class App : public std::enable_shared_from_this, std::string auth_route() REQUIRES(!m_route_mutex); std::string base_url() REQUIRES(!m_route_mutex); - bool verify_user_present(const std::shared_ptr& user) const; + bool verify_user_present(const std::shared_ptr& user) const REQUIRES(m_user_mutex); + + // UserProvider implementation + friend class User; + + util::CheckedMutex m_user_mutex; + mutable std::unordered_map m_users GUARDED_BY(m_user_mutex); + User* m_current_user GUARDED_BY(m_user_mutex) = nullptr; + + void register_sync_user(User& sync_user) REQUIRES(m_user_mutex); + void unregister_sync_user(User& user) REQUIRES(!m_user_mutex); + + // user helpers + std::shared_ptr get_user_for_id(const std::string& user_id) REQUIRES(m_user_mutex); + void user_data_updated(const std::string& user_id) REQUIRES(m_user_mutex); + void log_out(const std::shared_ptr& user, SyncUser::State new_state, + util::UniqueFunction)>&& completion) REQUIRES(!m_route_mutex); }; // MARK: Provider client templates diff --git a/src/realm/object-store/sync/app_config.hpp b/src/realm/object-store/sync/app_config.hpp new file mode 100644 index 00000000000..507cb39ec0f --- /dev/null +++ b/src/realm/object-store/sync/app_config.hpp @@ -0,0 +1,105 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or utilied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_SYNC_APP_CONFIG_HPP +#define REALM_OS_SYNC_APP_CONFIG_HPP + +#include +#include +#include +#include +#include + +namespace realm { +struct SyncClientTimeouts { + SyncClientTimeouts(); + // See sync::Client::Config for the meaning of these fields. + uint64_t connect_timeout; + uint64_t connection_linger_time; + uint64_t ping_keepalive_period; + uint64_t pong_keepalive_timeout; + uint64_t fast_reconnect_limit; +}; + +struct SyncClientConfig { + using LoggerFactory = std::function(util::Logger::Level)>; + LoggerFactory logger_factory; + util::Logger::Level log_level = util::Logger::Level::info; + ReconnectMode reconnect_mode = ReconnectMode::normal; // For internal sync-client testing only! +#if REALM_DISABLE_SYNC_MULTIPLEXING + bool multiplex_sessions = false; +#else + bool multiplex_sessions = true; +#endif + + // The SyncSocket instance used by the Sync Client for event synchronization + // and creating WebSockets. If not provided the default implementation will be used. + std::shared_ptr socket_provider; + + // Optional thread observer for event loop thread events in the default SyncSocketProvider + // implementation. It is not used for custom SyncSocketProvider implementations. + std::shared_ptr default_socket_provider_thread_observer; + + // {@ + // Optional information about the binding/application that is sent as part of the User-Agent + // when establishing a connection to the server. These values are only used by the default + // SyncSocket implementation. Custom SyncSocket implementations must update the User-Agent + // directly, if supported by the platform APIs. + std::string user_agent_binding_info; + std::string user_agent_application_info; + // @} + + SyncClientTimeouts timeouts; +}; + +namespace app { +struct AppConfig { + // Information about the device where the app is running + struct DeviceInfo { + std::string platform_version; // json: platformVersion + std::string sdk_version; // json: sdkVersion + std::string sdk; // json: sdk + std::string device_name; // json: deviceName + std::string device_version; // json: deviceVersion + std::string framework_name; // json: frameworkName + std::string framework_version; // json: frameworkVersion + std::string bundle_id; // json: bundleId + }; + + std::string app_id; + std::shared_ptr transport; + std::optional base_url; + std::optional default_request_timeout_ms; + DeviceInfo device_info; + + std::string base_file_path; + SyncClientConfig sync_client_config; + + enum class MetadataMode { + NoEncryption, // Enable metadata, but disable encryption. + Encryption, // Enable metadata, and use encryption (automatic if possible). + InMemory, // Do not persist metadata + }; + MetadataMode metadata_mode = MetadataMode::Encryption; + std::optional> custom_encryption_key; +}; + +} // namespace app +} // namespace realm + +#endif // REALM_OS_SYNC_APP_CONFIG_HPP diff --git a/src/realm/object-store/sync/app_service_client.hpp b/src/realm/object-store/sync/app_service_client.hpp index 9bd913432c4..922e9286435 100644 --- a/src/realm/object-store/sync/app_service_client.hpp +++ b/src/realm/object-store/sync/app_service_client.hpp @@ -26,8 +26,8 @@ #include namespace realm { -class SyncUser; namespace app { +class User; struct AppError; /// A class providing the core functionality necessary to make authenticated @@ -46,7 +46,7 @@ class AppServiceClient { /// the case of error. Using a string* rather than optional to avoid copying a potentially large /// string. virtual void - call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, + call_function(const std::shared_ptr& user, const std::string& name, std::string_view args_ejson, const util::Optional& service_name, util::UniqueFunction)>&& completion) = 0; @@ -58,7 +58,7 @@ class AppServiceClient { /// @param completion Returns the result from the intended call, will return an Optional AppError is an /// error is thrown and bson if successful virtual void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, const util::Optional& service_name, util::UniqueFunction&&, util::Optional)>&& completion) = 0; @@ -69,7 +69,7 @@ class AppServiceClient { /// @param completion Returns the result from the intended call, will return an Optional AppError is an /// error is thrown and bson if successful virtual void call_function( - const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, + const std::shared_ptr& user, const std::string& name, const bson::BsonArray& args_bson, util::UniqueFunction&&, util::Optional)>&& completion) = 0; /// Calls the Realm Cloud function with the provided name and arguments. diff --git a/src/realm/object-store/sync/app_user.cpp b/src/realm/object-store/sync/app_user.cpp new file mode 100644 index 00000000000..0739e8f28c8 --- /dev/null +++ b/src/realm/object-store/sync/app_user.cpp @@ -0,0 +1,309 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace realm::app { + +UserIdentity::UserIdentity(const std::string& id, const std::string& provider_type) + : id(id) + , provider_type(provider_type) +{ +} + +User::User(Private, std::shared_ptr app, std::string_view user_id) + : m_app(std::move(app)) + , m_app_id(m_app->config().app_id) + , m_user_id(user_id) +{ + m_app->register_sync_user(*this); +} + +User::~User() +{ + if (m_app) { + m_app->unregister_sync_user(*this); + } +} + +std::string User::user_id() const noexcept +{ + return m_user_id; +} + +std::string User::app_id() const noexcept +{ + return m_app_id; +} + +std::vector User::legacy_identities() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.legacy_identities; +} + +std::string User::access_token() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.access_token.token; +} + +std::string User::refresh_token() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.refresh_token.token; +} + +SyncUser::State User::state() const +{ + util::CheckedLockGuard lock(m_mutex); + if (!m_app) + return SyncUser::State::Removed; + return m_data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut; +} + +bool User::is_anonymous() const +{ + util::CheckedLockGuard lock(m_mutex); + return do_is_anonymous(); +} + +bool User::do_is_anonymous() const +{ + return m_data.access_token && m_data.identities.size() == 1 && + m_data.identities[0].provider_type == app::IdentityProviderAnonymous; +} + +std::string User::device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.device_id; +} + +bool User::has_device_id() const +{ + util::CheckedLockGuard lock(m_mutex); + return !m_data.device_id.empty() && m_data.device_id != "000000000000000000000000"; +} + +UserProfile User::user_profile() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.profile; +} + +std::vector User::identities() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.identities; +} + +std::optional User::custom_data() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_data.access_token.user_data; +} + +std::shared_ptr User::app() const +{ + util::CheckedLockGuard lock(m_mutex); + return m_app; +} + +SyncManager* User::sync_manager() +{ + util::CheckedLockGuard lock(m_mutex); + return m_app ? m_app->sync_manager().get() : nullptr; +} + +app::MongoClient User::mongo_client(const std::string& service_name) +{ + util::CheckedLockGuard lock(m_mutex); + return app::MongoClient(shared_from_this(), m_app->app_service_client(), service_name); +} + +bool User::access_token_refresh_required() const +{ + using namespace std::chrono; + constexpr size_t buffer_seconds = 5; // arbitrary + util::CheckedLockGuard lock(m_mutex); + const auto now = duration_cast(system_clock::now().time_since_epoch()).count() + + m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed); + const auto threshold = now - buffer_seconds; + return m_data.access_token && m_data.access_token.expires_at < static_cast(threshold); +} + +void User::log_out() +{ + if (auto app = this->app()) { + app->log_out(shared_from_this(), nullptr); + } +} + +void User::detach_and_tear_down() +{ + // it feels like there's probably a race condition with this being called + // at the wrong time as a sync session is being established on another thread + std::shared_ptr sync_manager; + { + util::CheckedLockGuard lk(m_mutex); + m_data.access_token.token.clear(); + m_data.refresh_token.token.clear(); + if (m_app) { + sync_manager = m_app->sync_manager(); + m_app->unregister_sync_user(*this); + } + m_app.reset(); + } + + if (sync_manager) { + sync_manager->update_sessions_for(*this, SyncUser::State::LoggedIn, SyncUser::State::Removed, {}); + } +} + +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 (!m_app) { + return; // already detached + } + 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); + REALM_ASSERT(m_app); + sync_manager = m_app->sync_manager(); + old_state = m_data.access_token ? SyncUser::State::LoggedIn : SyncUser::State::LoggedOut; + if (new_state == SyncUser::State::LoggedIn && data->access_token != m_data.access_token) + new_token = data->access_token.token; + m_data = std::move(*data); + } + + sync_manager->update_sessions_for(*this, old_state, new_state, new_token); + emit_change_to_subscribers(*this); +} + +void User::request_log_out() +{ + if (auto app = this->app()) { + auto new_state = is_anonymous() ? SyncUser::State::Removed : SyncUser::State::LoggedOut; + app->m_metadata_store->log_out(m_user_id, new_state); + update_backing_data(app->m_metadata_store->get_user(m_user_id)); + } +} + +void User::request_refresh_user(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + app->get_profile(shared_from_this(), [completion = std::move(completion)](auto, auto error) { + completion(std::move(error)); + }); + } +} + +void User::request_refresh_location(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + bool update_location = true; + app->refresh_access_token(shared_from_this(), update_location, std::move(completion)); + } +} + +void User::request_access_token(util::UniqueFunction)>&& completion) +{ + if (auto app = this->app()) { + bool update_location = false; + app->refresh_access_token(shared_from_this(), update_location, std::move(completion)); + } +} + +void User::track_realm(std::string_view path) +{ + if (auto app = this->app()) { + app->m_metadata_store->add_realm_path(m_user_id, path); + } +} + +std::string User::create_file_action(SyncFileAction action, std::string_view original_path, + std::optional requested_recovery_dir) +{ + if (auto app = this->app()) { + std::string recovery_path; + if (action == SyncFileAction::BackUpThenDeleteRealm) { + recovery_path = + util::reserve_unique_file_name(app->m_file_manager->recovery_directory_path(requested_recovery_dir), + util::create_timestamped_template("recovered_realm")); + } + app->m_metadata_store->create_file_action(action, original_path, recovery_path); + return recovery_path; + } + return ""; +} + +void User::refresh_custom_data(util::UniqueFunction)> completion_block) + REQUIRES(!m_mutex) +{ + refresh_custom_data(false, std::move(completion_block)); +} + +void User::refresh_custom_data(bool update_location, + util::UniqueFunction)> completion_block) +{ + if (auto app = this->app()) { + app->refresh_custom_data(shared_from_this(), update_location, std::move(completion_block)); + } + else { + completion_block(app::AppError( + ErrorCodes::ClientUserNotFound, + util::format("Cannot initiate a refresh on user '%1' because the user has been removed", m_user_id))); + } +} +} // 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..7d6277c49ce --- /dev/null +++ b/src/realm/object-store/sync/app_user.hpp @@ -0,0 +1,257 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_APP_USER_HPP +#define REALM_OS_APP_USER_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace realm::app { +class App; +struct AppError; +class MetadataStore; +class MongoClient; + +struct UserProfile { + // The full name of the user. + 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 + { + auto it = m_data.find(name); + if (it == m_data.end()) { + return util::none; + } + return static_cast((*it).second); + } +}; + +// A struct that represents an identity that a `User` is linked to +struct UserIdentity { + // the id of the identity + std::string id; + // the associated provider type of the identity + std::string provider_type; + + UserIdentity(const std::string& id, const std::string& provider_type); + + bool operator==(const UserIdentity& other) const + { + return id == other.id && provider_type == other.provider_type; + } + + bool operator!=(const UserIdentity& other) const + { + return id != other.id || provider_type != other.provider_type; + } +}; + +struct UserData { + // 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); + + // ------------------------------------------------------------------------ + // 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; + const std::string m_app_id; + const std::string m_user_id; + UserData m_data GUARDED_BY(m_mutex); + std::atomic m_seconds_to_adjust_time_for_testing = 0; + + bool do_is_anonymous() const REQUIRES(m_mutex); +}; + +} // namespace realm::app + +namespace std { +template <> +struct hash { + size_t operator()(realm::app::UserIdentity const&) const; +}; +} // namespace std + +#endif // REALM_OS_SYNC_USER_HPP diff --git a/src/realm/object-store/sync/app_utils.cpp b/src/realm/object-store/sync/app_utils.cpp index 8ed703426ea..10ea4eb6ef8 100644 --- a/src/realm/object-store/sync/app_utils.cpp +++ b/src/realm/object-store/sync/app_utils.cpp @@ -79,7 +79,7 @@ std::optional AppUtils::check_for_errors(const Response& response) try { auto ct = find_header("content-type", response.headers); - if (ct && ct->second == "application/json") { + if (ct && ct->second == "application/json" && !response.body.empty()) { auto body = nlohmann::json::parse(response.body); auto message = body.find("error"); auto link = body.find("link"); diff --git a/src/realm/object-store/sync/auth_request_client.hpp b/src/realm/object-store/sync/auth_request_client.hpp index 824246af9bd..8f0bc763adb 100644 --- a/src/realm/object-store/sync/auth_request_client.hpp +++ b/src/realm/object-store/sync/auth_request_client.hpp @@ -24,8 +24,8 @@ #include namespace realm { -class SyncUser; namespace app { +class User; struct Request; struct Response; @@ -35,7 +35,7 @@ class AuthRequestClient { virtual std::string url_for_path(const std::string& path) const = 0; - virtual void do_authenticated_request(Request&&, const std::shared_ptr& sync_user, + virtual void do_authenticated_request(Request&&, const std::shared_ptr& sync_user, util::UniqueFunction&&) = 0; }; diff --git a/src/realm/object-store/sync/impl/app_metadata.cpp b/src/realm/object-store/sync/impl/app_metadata.cpp new file mode 100644 index 00000000000..a0416f21b80 --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.cpp @@ -0,0 +1,927 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include +#include +#include +#if REALM_PLATFORM_APPLE +#include +#endif + +#include +#include +#include + +using namespace realm; +using realm::app::UserData; + +namespace { + +struct CurrentUserSchema { + TableKey table_key; + ColKey user_id; + + static constexpr const char* table_name = "current_user_identity"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id = object_schema->persisted_properties[0].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, {{table_name, PropertyType::String}}}; + } +}; + +struct UserIdentitySchema { + TableKey table_key; + ColKey user_id; + ColKey provider_id; + + static constexpr const char* table_name = "UserIdentity"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id = object_schema->persisted_properties[0].column_key; + provider_id = object_schema->persisted_properties[1].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + ObjectSchema::ObjectType::Embedded, + { + {"id", PropertyType::String}, + {"provider_type", PropertyType::String}, + }}; + } +}; + +struct SyncUserSchema { + TableKey table_key; + + // The server-supplied user_id for the user. Unique per server instance. + ColKey user_id_col; + // Locally generated UUIDs for the user. These are tracked to be able + // to open pre-existing Realm files, but are no longer generated or + // used for anything else. + ColKey legacy_uuids_col; + // The cached refresh token for this user. + ColKey refresh_token_col; + // The cached access token for this user. + ColKey access_token_col; + // The identities for this user. + ColKey identities_col; + // The current state of this user. + ColKey state_col; + // The device id of this user. + ColKey device_id_col; + // Any additional profile attributes, formatted as a bson string. + ColKey profile_dump_col; + // The set of absolute file paths to Realms belonging to this user. + ColKey realm_file_paths_col; + + static constexpr const char* table_name = "UserMetadata"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + user_id_col = object_schema->persisted_properties[0].column_key; + legacy_uuids_col = object_schema->persisted_properties[1].column_key; + refresh_token_col = object_schema->persisted_properties[2].column_key; + access_token_col = object_schema->persisted_properties[3].column_key; + identities_col = object_schema->persisted_properties[4].column_key; + state_col = object_schema->persisted_properties[5].column_key; + device_id_col = object_schema->persisted_properties[6].column_key; + profile_dump_col = object_schema->persisted_properties[7].column_key; + realm_file_paths_col = object_schema->persisted_properties[8].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + {{"identity", PropertyType::String}, + {"legacy_uuids", PropertyType::String | PropertyType::Array}, + {"refresh_token", PropertyType::String | PropertyType::Nullable}, + {"access_token", PropertyType::String | PropertyType::Nullable}, + {"identities", PropertyType::Object | PropertyType::Array, UserIdentitySchema::table_name}, + {"state", PropertyType::Int}, + {"device_id", PropertyType::String}, + {"profile_data", PropertyType::String}, + {"local_realm_paths", PropertyType::Set | PropertyType::String}}}; + } +}; + +struct FileActionSchema { + TableKey table_key; + + // The original path on disk of the file (generally, the main file for an on-disk Realm). + ColKey idx_original_name; + // A new path on disk for a file to be written to. Context-dependent. + ColKey idx_new_name; + // An enum describing the action to take. + ColKey idx_action; + // The partition key of the Realm. + ColKey idx_partition; + // The user_id of the user to whom the file action applies (despite the internal column name). + ColKey idx_user_identity; + + static constexpr const char* table_name = "FileActionMetadata"; + + void read(Realm& realm) + { + auto object_schema = realm.schema().find(table_name); + table_key = object_schema->table_key; + idx_original_name = object_schema->persisted_properties[0].column_key; + idx_new_name = object_schema->persisted_properties[1].column_key; + idx_action = object_schema->persisted_properties[2].column_key; + idx_partition = object_schema->persisted_properties[3].column_key; + idx_user_identity = object_schema->persisted_properties[4].column_key; + } + + static ObjectSchema object_schema() + { + return {table_name, + { + {"original_name", PropertyType::String, Property::IsPrimary{true}}, + {"new_name", PropertyType::String | PropertyType::Nullable}, + {"action", PropertyType::Int}, + {"url", PropertyType::String}, // actually partition key + {"identity", PropertyType::String}, // actually user id + }}; + } +}; + +void migrate_to_v7(std::shared_ptr old_realm, std::shared_ptr realm) +{ + // Before schema version 7 there may have been multiple UserMetadata entries + // for a single user_id with different provider types, so we need to merge + // any duplicates together + + SyncUserSchema schema; + schema.read(*realm); + + TableRef table = realm->read_group().get_table(schema.table_key); + TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), SyncUserSchema::table_name); + if (table->is_empty()) + return; + REALM_ASSERT(table->size() == old_table->size()); + + ColKey old_uuid_col = old_table->get_column_key("local_uuid"); + + std::unordered_map users; + for (size_t i = 0, j = 0; i < table->size(); ++j) { + auto obj = table->get_object(i); + + // Move the local uuid from the old column to the list + auto old_obj = old_table->get_object(j); + obj.get_list(schema.legacy_uuids_col).add(old_obj.get(old_uuid_col)); + + // Check if we've already seen an object with the same id. If not, store + // this one and move on + std::string user_id = obj.get(schema.user_id_col); + auto& existing = users[obj.get(schema.user_id_col)]; + if (!existing.is_valid()) { + existing = obj; + ++i; + continue; + } + + // We have a second object for the same id, so we need to merge them. + // First we merge the state: if one is logged in and the other isn't, + // we'll use the logged-in state and tokens. If both are logged in, we'll + // use the more recent login. If one is logged out and the other is + // removed we'll use the logged out state. If both are logged out or + // both are removed then it doesn't matter which we pick. + using State = SyncUser::State; + auto state = State(obj.get(schema.state_col)); + auto existing_state = State(existing.get(schema.state_col)); + if (state == existing_state) { + if (state == State::LoggedIn) { + RealmJWT token_1(existing.get(schema.access_token_col)); + RealmJWT token_2(obj.get(schema.access_token_col)); + if (token_1.issued_at < token_2.issued_at) { + existing.set(schema.refresh_token_col, obj.get(schema.refresh_token_col)); + existing.set(schema.access_token_col, obj.get(schema.access_token_col)); + } + } + } + else if (state == State::LoggedIn || existing_state == State::Removed) { + existing.set(schema.state_col, int64_t(state)); + existing.set(schema.refresh_token_col, obj.get(schema.refresh_token_col)); + existing.set(schema.access_token_col, obj.get(schema.access_token_col)); + } + + // Next we merge the list properties (identities, legacy uuids, realm file paths) + { + auto dest = existing.get_linklist(schema.identities_col); + auto src = obj.get_linklist(schema.identities_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_list(schema.legacy_uuids_col); + auto src = obj.get_list(schema.legacy_uuids_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + if (dest.find_first(src.get(i)) == npos) { + dest.add(src.get(i)); + } + } + } + { + auto dest = existing.get_set(schema.realm_file_paths_col); + auto src = obj.get_set(schema.realm_file_paths_col); + for (size_t i = 0, size = src.size(); i < size; ++i) { + dest.insert(src.get(i)); + } + } + + // Finally we delete the duplicate object. We don't increment `i` as it's + // now the index of the object just after the one we're deleting. + obj.remove(); + } +} + +std::shared_ptr try_get_realm(const RealmConfig& config) +{ + try { + return Realm::get_shared_realm(config); + } + catch (const InvalidDatabase&) { + return nullptr; + } +} + +std::shared_ptr open_realm(RealmConfig& config, bool should_encrypt, bool caller_supplied_key) +{ + if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { + if (auto realm = try_get_realm(config)) + return realm; + + // Encryption key changed, so delete the existing metadata realm and + // recreate it + util::File::remove(config.path); + return try_get_realm(config); + } + +#if REALM_PLATFORM_APPLE + // This logic is all a giant race condition once we have multi-process sync. + // Wrapping it all (including the keychain accesses) in DB::call_with_lock() + // might suffice. + + // First try to open the Realm with a key already stored in the keychain. + // This works for both the case where everything is sensible and valid and + // when we have a key but no metadata Realm. + auto key = keychain::get_existing_metadata_realm_key(); + if (key) { + config.encryption_key = *key; + if (auto realm = try_get_realm(config)) + return realm; + } + + // If we have an existing file and either no key or the key didn't work to + // decrypt it, then we might have an unencrypted metadata Realm resulting + // from a previous run being unable to access the keychain. + if (util::File::exists(config.path)) { + config.encryption_key.clear(); + if (auto realm = try_get_realm(config)) + return realm; + + // We weren't able to open the existing file with either the stored key + // or no key, so just delete it. + util::File::remove(config.path); + } + + // We now have no metadata Realm. If we don't have an existing stored key, + // try to create and store a new one. This might fail, in which case we + // just create an unencrypted Realm file. + if (!key) + key = keychain::create_new_metadata_realm_key(); + if (key) + config.encryption_key = std::move(*key); + return try_get_realm(config); +#else // REALM_PLATFORM_APPLE + REALM_UNREACHABLE(); +#endif // REALM_PLATFORM_APPLE +} + +struct PersistedSyncMetadataManager : public app::MetadataStore { + RealmConfig m_config; + SyncUserSchema m_user_schema; + FileActionSchema m_file_action_schema; + UserIdentitySchema m_user_identity_schema; + CurrentUserSchema m_current_user_schema; + + PersistedSyncMetadataManager(std::string path, bool should_encrypt, + util::Optional> encryption_key, SyncFileManager& file_manager) + { + constexpr uint64_t SCHEMA_VERSION = 7; + + if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) + throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); + + m_config.automatic_change_notifications = false; + m_config.path = std::move(path); + m_config.schema = Schema{ + UserIdentitySchema::object_schema(), + SyncUserSchema::object_schema(), + FileActionSchema::object_schema(), + CurrentUserSchema::object_schema(), + }; + + m_config.schema_version = SCHEMA_VERSION; + m_config.schema_mode = SchemaMode::Automatic; + m_config.scheduler = util::Scheduler::make_dummy(); + if (encryption_key) + m_config.encryption_key = std::move(*encryption_key); + m_config.automatically_handle_backlinks_in_migrations = true; + m_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, Schema&) { + if (old_realm->schema_version() < 7) { + migrate_to_v7(old_realm, realm); + } + }; + + auto realm = open_realm(m_config, should_encrypt, encryption_key != none); + m_user_schema.read(*realm); + m_file_action_schema.read(*realm); + m_user_identity_schema.read(*realm); + m_current_user_schema.read(*realm); + + realm->begin_transaction(); + perform_file_actions(*realm, file_manager); + remove_dead_users(*realm, file_manager); + realm->commit_transaction(); + } + + std::shared_ptr get_realm() const + { + return Realm::get_shared_realm(m_config); + } + + void remove_dead_users(Realm& realm, SyncFileManager& file_manager) + { + auto& schema = m_user_schema; + TableRef table = realm.read_group().get_table(schema.table_key); + for (auto obj : *table) { + if (static_cast(obj.get(schema.state_col)) == SyncUser::State::Removed) { + delete_user_realms(file_manager, obj); + } + } + } + + void delete_user_realms(SyncFileManager& file_manager, Obj& obj) + { + Set paths = obj.get_set(m_user_schema.realm_file_paths_col); + bool any_failed = false; + for (auto path : paths) { + if (!file_manager.remove_realm(path)) + any_failed = true; + } + try { + file_manager.remove_user_realms(obj.get(m_user_schema.user_id_col)); + } + catch (FileAccessError const&) { + any_failed = true; + } + + // Only remove the object if all of the tracked realms no longer exist, + // and otherwise try again to delete them on the next launch + if (!any_failed) { + obj.remove(); + } + } + + bool perform_file_action(SyncFileManager& file_manager, Obj& obj) + { + auto& schema = m_file_action_schema; + switch (static_cast(obj.get(schema.idx_action))) { + case SyncFileAction::DeleteRealm: + // Delete all the files for the given Realm. + return file_manager.remove_realm(obj.get(schema.idx_original_name)); + + case SyncFileAction::BackUpThenDeleteRealm: + // Copy the primary Realm file to the recovery dir, and then delete the Realm. + auto new_name = obj.get(schema.idx_new_name); + auto original_name = obj.get(schema.idx_original_name); + if (!util::File::exists(original_name)) { + // The Realm file doesn't exist anymore, which is fine + return true; + } + + if (new_name && !util::File::exists(new_name) && + file_manager.copy_realm_file(original_name, new_name)) { + // We successfully copied the Realm file to the recovery directory. + bool did_remove = file_manager.remove_realm(original_name); + // if the copy succeeded but not the delete, then running BackupThenDelete + // a second time would fail, so change this action to just delete the original file. + if (did_remove) { + return true; + } + obj.set(schema.idx_action, static_cast(SyncFileAction::DeleteRealm)); + } + } + return false; + } + + void perform_file_actions(Realm& realm, SyncFileManager& file_manager) + { + TableRef table = realm.read_group().get_table(m_file_action_schema.table_key); + if (table->is_empty()) + return; + + for (auto 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.emplace(std::pair(path, FileAction{action, std::string(backup_path)})); + } +}; + +} // anonymous namespace + +app::MetadataStore::~MetadataStore() = default; + +std::unique_ptr app::create_metadata_store(const AppConfig& config, SyncFileManager& file_manager) +{ + if (config.metadata_mode == AppConfig::MetadataMode::InMemory) { + return std::make_unique(); + } + return std::make_unique( + file_manager.metadata_path(), config.metadata_mode != AppConfig::MetadataMode::NoEncryption, + config.custom_encryption_key, file_manager); +} diff --git a/src/realm/object-store/sync/impl/app_metadata.hpp b/src/realm/object-store/sync/impl/app_metadata.hpp new file mode 100644 index 00000000000..8972b1406b3 --- /dev/null +++ b/src/realm/object-store/sync/impl/app_metadata.hpp @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#ifndef REALM_OS_APP_BACKING_STORE_HPP +#define REALM_OS_APP_BACKING_STORE_HPP + +#include +#include +#include + +#include +#include +#include +#include + +namespace realm { +class SyncFileManager; + +namespace app { +class App; + +class MetadataStore { +public: + virtual ~MetadataStore(); + + // Attempt to perform all pending file actions for the given path. Returns + // true if any were performed. + virtual bool immediately_run_file_actions(SyncFileManager& fm, std::string_view realm_path) = 0; + + virtual void create_file_action(SyncFileAction action, std::string_view original_path, + std::string_view recovery_path) = 0; + + virtual bool has_logged_in_user(std::string_view user_id) = 0; + virtual std::optional get_user(std::string_view user_id) = 0; + + // Create a user if no user with this id exists, or update only the given + // fields if one does + virtual void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token, + std::string_view device_id) = 0; + + // Update the stored data for an existing user + virtual void update_user(std::string_view user_id, const UserData& data) = 0; + + // Discard tokens, set state to the given one, and if the user is the current + // user set it to the new active user + virtual void log_out(std::string_view user_id, SyncUser::State new_state) = 0; + virtual void delete_user(SyncFileManager& file_manager, std::string_view user_id) = 0; + + virtual std::string get_current_user() = 0; + virtual void set_current_user(std::string_view user_id) = 0; + + virtual std::vector get_all_users() = 0; + + virtual void add_realm_path(std::string_view user_id, std::string_view path) = 0; +}; + +std::unique_ptr create_metadata_store(const AppConfig& config, SyncFileManager& file_manager); + +} // namespace app +} // namespace realm + +#endif // REALM_OS_APP_BACKING_STORE_HPP diff --git a/src/realm/object-store/sync/impl/sync_client.hpp b/src/realm/object-store/sync/impl/sync_client.hpp index 73abfa1b10f..6875158f962 100644 --- a/src/realm/object-store/sync/impl/sync_client.hpp +++ b/src/realm/object-store/sync/impl/sync_client.hpp @@ -37,8 +37,7 @@ #include #endif -namespace realm { -namespace _impl { +namespace realm::_impl { struct SyncClient { SyncClient(const std::shared_ptr& logger, SyncClientConfig const& config, @@ -138,8 +137,6 @@ struct SyncClient { return m_client.notify_session_terminated(); } - ~SyncClient() {} - private: std::shared_ptr m_socket_provider; sync::Client m_client; @@ -150,7 +147,6 @@ struct SyncClient { #endif }; -} // namespace _impl -} // namespace realm +} // namespace realm::_impl #endif // REALM_OS_SYNC_CLIENT_HPP diff --git a/src/realm/object-store/sync/impl/sync_file.cpp b/src/realm/object-store/sync/impl/sync_file.cpp index 84e9a3ecdc2..e022577a50a 100644 --- a/src/realm/object-store/sync/impl/sync_file.cpp +++ b/src/realm/object-store/sync/impl/sync_file.cpp @@ -18,7 +18,11 @@ #include +#include + #include +#include +#include #include #include #include @@ -237,9 +241,10 @@ static std::string validate_and_clean_path(const std::string& path) } // namespace util -SyncFileManager::SyncFileManager(const std::string& base_path, const std::string& app_id) - : m_base_path(util::file_path_by_appending_component(base_path, c_sync_directory, util::FilePathType::Directory)) - , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(app_id), +SyncFileManager::SyncFileManager(const app::AppConfig& config) + : m_base_path(util::file_path_by_appending_component(config.base_file_path, c_sync_directory, + util::FilePathType::Directory)) + , m_app_path(util::file_path_by_appending_component(m_base_path, util::validate_and_clean_path(config.app_id), util::FilePathType::Directory)) { util::try_make_dir(m_base_path); @@ -253,21 +258,17 @@ std::string SyncFileManager::get_special_directory(std::string directory_name) c return dir_path; } -std::string SyncFileManager::user_directory(const std::string& user_identity) const +std::string SyncFileManager::user_directory(const std::string& user_id) const { - std::string user_path = get_user_directory_path(user_identity); + std::string user_path = get_user_directory_path(user_id); util::try_make_dir(user_path); return user_path; } -void SyncFileManager::remove_user_realms(const std::string& user_identity, - const std::vector& realm_paths) const +void SyncFileManager::remove_user_realms(const std::string& user_id) const { - for (auto& path : realm_paths) { - remove_realm(path); - } // The following is redundant except for apps built before file tracking. - std::string user_path = get_user_directory_path(user_identity); + std::string user_path = get_user_directory_path(user_id); util::try_remove_dir_recursive(user_path); } @@ -298,11 +299,10 @@ bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::st return true; } -bool SyncFileManager::remove_realm(const std::string& user_identity, - const std::vector& legacy_user_identities, +bool SyncFileManager::remove_realm(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& raw_realm_path, const std::string& partition) const { - auto existing = get_existing_realm_file_path(user_identity, legacy_user_identities, raw_realm_path, partition); + auto existing = get_existing_realm_file_path(user_id, legacy_user_identities, raw_realm_path, partition); if (existing) { return remove_realm(*existing); } @@ -331,11 +331,11 @@ static bool try_file_remove(const std::string& path) noexcept } util::Optional -SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, +SyncFileManager::get_existing_realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const { - std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name); + std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_id, realm_file_name); if (try_file_exists(preferred_name_without_suffix)) { return preferred_name_without_suffix; } @@ -362,7 +362,7 @@ SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, // We used to hash the string value of the partition. For compatibility, check that SHA256 // hash file name exists, and if it does, continue to use it. if (!partition.empty()) { - std::string hashed_partition_path = legacy_hashed_partition_path(user_identity, partition); + std::string hashed_partition_path = legacy_hashed_partition_path(user_id, partition); if (try_file_exists(hashed_partition_path)) { return hashed_partition_path; } @@ -384,12 +384,11 @@ SyncFileManager::get_existing_realm_file_path(const std::string& user_identity, return util::none; } -std::string SyncFileManager::realm_file_path(const std::string& user_identity, +std::string SyncFileManager::realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const { - auto existing_path = - get_existing_realm_file_path(user_identity, legacy_user_identities, realm_file_name, partition); + auto existing_path = get_existing_realm_file_path(user_id, legacy_user_identities, realm_file_name, partition); if (existing_path) { return *existing_path; } @@ -397,7 +396,7 @@ std::string SyncFileManager::realm_file_path(const std::string& user_identity, // since this appears to be a new file, test the normal location // we use a test file with the same name and a suffix of the // same length, so we can catch "filename too long" errors on windows - std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_identity, realm_file_name); + std::string preferred_name_without_suffix = preferred_realm_path_without_suffix(user_id, realm_file_name); std::string preferred_name_with_suffix = preferred_name_without_suffix + c_realm_file_suffix; try { std::string test_path = preferred_name_without_suffix + c_realm_file_test_suffix; @@ -453,12 +452,11 @@ bool SyncFileManager::remove_metadata_realm() const } } -std::string SyncFileManager::preferred_realm_path_without_suffix(const std::string& user_identity, +std::string SyncFileManager::preferred_realm_path_without_suffix(const std::string& user_id, const std::string& realm_file_name) const { auto escaped_file_name = util::validate_and_clean_path(realm_file_name); - std::string preferred_name = - util::file_path_by_appending_component(user_directory(user_identity), escaped_file_name); + std::string preferred_name = util::file_path_by_appending_component(user_directory(user_id), escaped_file_name); if (StringData(preferred_name).ends_with(c_realm_file_suffix)) { preferred_name = preferred_name.substr(0, preferred_name.size() - strlen(c_realm_file_suffix)); } @@ -474,14 +472,14 @@ std::string SyncFileManager::fallback_hashed_realm_file_path(const std::string& return hashed_name; } -std::string SyncFileManager::legacy_hashed_partition_path(const std::string& user_identity, +std::string SyncFileManager::legacy_hashed_partition_path(const std::string& user_id, const std::string& partition) const { std::array hash; util::sha256(partition.data(), partition.size(), hash.data()); std::string legacy_hashed_file_name = util::hex_dump(hash.data(), hash.size(), ""); std::string legacy_partition_path = util::file_path_by_appending_component( - get_user_directory_path(user_identity), legacy_hashed_file_name + c_realm_file_suffix); + get_user_directory_path(user_id), legacy_hashed_file_name + c_realm_file_suffix); return legacy_partition_path; } @@ -507,10 +505,62 @@ std::string SyncFileManager::legacy_local_identity_path(const std::string& local return path; } -std::string SyncFileManager::get_user_directory_path(const std::string& user_identity) const +std::string SyncFileManager::get_user_directory_path(const std::string& user_id) const { - return file_path_by_appending_component(m_app_path, util::validate_and_clean_path(user_identity), + return file_path_by_appending_component(m_app_path, util::validate_and_clean_path(user_id), util::FilePathType::Directory); } +struct UnsupportedBsonPartition : public std::logic_error { + UnsupportedBsonPartition(std::string msg) + : std::logic_error(msg) + { + } +}; + +static std::string string_from_partition(std::string_view partition) +{ + bson::Bson partition_value = bson::parse(partition); + switch (partition_value.type()) { + case bson::Bson::Type::Int32: + return util::format("i_%1", static_cast(partition_value)); + case bson::Bson::Type::Int64: + return util::format("l_%1", static_cast(partition_value)); + case bson::Bson::Type::String: + return util::format("s_%1", static_cast(partition_value)); + case bson::Bson::Type::ObjectId: + return util::format("o_%1", static_cast(partition_value).to_string()); + case bson::Bson::Type::Uuid: + return util::format("u_%1", static_cast(partition_value).to_string()); + case bson::Bson::Type::Null: + return "null"; + default: + throw UnsupportedBsonPartition(util::format("Unsupported partition key value: '%1'. Only int, string " + "UUID and ObjectId types are currently supported.", + partition_value.to_string())); + } +} + +std::string SyncFileManager::path_for_realm(const SyncConfig& config, + std::optional custom_file_name) const +{ + auto user = config.user; + REALM_ASSERT(user); + // Attempt to make a nicer filename which will ease debugging when + // locating files in the filesystem. + auto file_name = [&]() -> std::string { + if (custom_file_name) { + return *custom_file_name; + } + if (config.flx_sync_requested) { + REALM_ASSERT_DEBUG(config.partition_value.empty()); + return "flx_sync_default"; + } + return string_from_partition(config.partition_value); + }(); + auto path = realm_file_path(user->user_id(), user->legacy_identities(), file_name, config.partition_value); + user->track_realm(path); + return path; +} + } // namespace realm diff --git a/src/realm/object-store/sync/impl/sync_file.hpp b/src/realm/object-store/sync/impl/sync_file.hpp index 7750ae85748..c5191149e2e 100644 --- a/src/realm/object-store/sync/impl/sync_file.hpp +++ b/src/realm/object-store/sync/impl/sync_file.hpp @@ -19,13 +19,17 @@ #ifndef REALM_OS_SYNC_FILE_HPP #define REALM_OS_SYNC_FILE_HPP +#include #include - -#include - -#include +#include namespace realm { +struct SyncConfig; +class SyncUser; + +namespace app { +struct AppConfig; +} namespace util { @@ -58,27 +62,32 @@ std::string reserve_unique_file_name(const std::string& path, const std::string& // This class manages how Synced Realms are stored on the filesystem. class SyncFileManager { public: - SyncFileManager(const std::string& base_path, const std::string& app_id); + SyncFileManager(const app::AppConfig&); /// Remove the Realms at the specified absolute paths along with any associated helper files. - void remove_user_realms(const std::string& user_identity, - const std::vector& realm_paths) const; // throws + void remove_user_realms(const std::string& user_id) const; // throws /// A non throw version of File::exists(), returning false if any exceptions are thrown when attempting to access /// this file. static bool try_file_exists(const std::string& path) noexcept; - util::Optional get_existing_realm_file_path(const std::string& user_identity, - const std::vector& legacy_user_identities, - const std::string& realm_file_name, - const std::string& partition) const; + std::optional get_existing_realm_file_path(const std::string& user_id, + const std::vector& legacy_user_identities, + const std::string& realm_file_name, + const std::string& partition) const; /// Return the path for a given Realm, creating the user directory if it does not already exist. - std::string realm_file_path(const std::string& user_identity, - const std::vector& legacy_user_identities, + std::string realm_file_path(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; + // Get the default path for a Realm for the given configuration. + // The default value is `///.realm`. + // If the file cannot be created at this location, for example due to path length restrictions, + // this function may pass back `/.realm` + std::string path_for_realm(const SyncConfig& config, + std::optional custom_file_name = std::nullopt) const; + /// Remove the Realm at a given path for a given user. Returns `true` if the remove operation fully succeeds. - bool remove_realm(const std::string& user_identity, const std::vector& legacy_user_identities, + bool remove_realm(const std::string& user_id, const std::vector& legacy_user_identities, const std::string& realm_file_name, const std::string& partition) const; /// Remove the Realm whose primary Realm file is located at `absolute_path`. Returns `true` if the remove @@ -104,7 +113,7 @@ class SyncFileManager { return m_app_path; } - std::string recovery_directory_path(util::Optional const& directory = none) const + std::string recovery_directory_path(std::optional const& directory = {}) const { return get_special_directory(directory.value_or(c_recovery_directory)); } @@ -134,15 +143,15 @@ class SyncFileManager { return get_special_directory(c_utility_directory); } /// Return the user directory for a given user, creating it if it does not already exist. - std::string user_directory(const std::string& identity) const; + std::string user_directory(const std::string& user_id) const; // Construct the absolute path to the users directory - std::string get_user_directory_path(const std::string& user_identity) const; - std::string legacy_hashed_partition_path(const std::string& user_identity, const std::string& partition) const; + std::string get_user_directory_path(const std::string& user_id) const; + std::string legacy_hashed_partition_path(const std::string& user_id, const std::string& partition) const; std::string legacy_realm_file_path(const std::string& local_user_identity, const std::string& realm_file_name) const; std::string legacy_local_identity_path(const std::string& local_user_identity, const std::string& realm_file_name) const; - std::string preferred_realm_path_without_suffix(const std::string& user_identity, + std::string preferred_realm_path_without_suffix(const std::string& user_id, const std::string& realm_file_name) const; std::string fallback_hashed_realm_file_path(const std::string& preferred_path) const; }; diff --git a/src/realm/object-store/sync/impl/sync_metadata.cpp b/src/realm/object-store/sync/impl/sync_metadata.cpp deleted file mode 100644 index 81cafc2e97d..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ /dev/null @@ -1,845 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#if REALM_PLATFORM_APPLE -#include -#endif - -#include -#include -#include - -using namespace realm; - -namespace { -static const char* const c_sync_userMetadata = "UserMetadata"; -static const char* const c_sync_identityMetadata = "UserIdentity"; - -static const char* const c_sync_current_user_identity = "current_user_identity"; - -/* User keys */ -static const char* const c_sync_identity = "identity"; -static const char* const c_sync_legacy_uuids = "legacy_uuids"; -static const char* const c_sync_refresh_token = "refresh_token"; -static const char* const c_sync_access_token = "access_token"; -static const char* const c_sync_identities = "identities"; -static const char* const c_sync_state = "state"; -static const char* const c_sync_device_id = "device_id"; -static const char* const c_sync_profile_data = "profile_data"; -static const char* const c_sync_local_realm_paths = "local_realm_paths"; - -/* Identity keys */ -static const char* const c_sync_user_id = "id"; -static const char* const c_sync_provider_type = "provider_type"; - -static const char* const c_sync_fileActionMetadata = "FileActionMetadata"; -static const char* const c_sync_original_name = "original_name"; -static const char* const c_sync_new_name = "new_name"; -static const char* const c_sync_action = "action"; -static const char* const c_sync_partition = "url"; - -realm::Schema make_schema() -{ - using namespace realm; - return Schema{ - {c_sync_identityMetadata, - ObjectSchema::ObjectType::Embedded, - { - {c_sync_user_id, PropertyType::String}, - {c_sync_provider_type, PropertyType::String}, - }}, - {c_sync_userMetadata, - {{c_sync_identity, PropertyType::String}, - {c_sync_legacy_uuids, PropertyType::String | PropertyType::Array}, - {c_sync_refresh_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_access_token, PropertyType::String | PropertyType::Nullable}, - {c_sync_identities, PropertyType::Object | PropertyType::Array, c_sync_identityMetadata}, - {c_sync_state, PropertyType::Int}, - {c_sync_device_id, PropertyType::String}, - {c_sync_profile_data, PropertyType::String}, - {c_sync_local_realm_paths, PropertyType::Set | PropertyType::String}}}, - {c_sync_fileActionMetadata, - { - {c_sync_original_name, PropertyType::String, Property::IsPrimary{true}}, - {c_sync_new_name, PropertyType::String | PropertyType::Nullable}, - {c_sync_action, PropertyType::Int}, - {c_sync_partition, PropertyType::String}, // unused and should be removed in v8 - {c_sync_identity, PropertyType::String}, // unused and should be removed in v8 - }}, - {c_sync_current_user_identity, - { - {c_sync_current_user_identity, PropertyType::String}, - }}, - }; -} - -void migrate_to_v7(Realm& old_realm, Realm& realm) -{ - // Before schema version 7 there may have been multiple UserMetadata entries - // for a single user_id with different provider types, so we need to merge - // any duplicates together - - TableRef table = ObjectStore::table_for_object_type(realm.read_group(), c_sync_userMetadata); - TableRef old_table = ObjectStore::table_for_object_type(old_realm.read_group(), c_sync_userMetadata); - if (table->is_empty()) - return; - REALM_ASSERT(table->size() == old_table->size()); - - ColKey id_col = table->get_column_key(c_sync_identity); - ColKey old_uuid_col = old_table->get_column_key("local_uuid"); - ColKey new_uuid_col = table->get_column_key(c_sync_legacy_uuids); - ColKey state_col = table->get_column_key(c_sync_state); - - std::unordered_map users; - for (size_t i = 0, j = 0; i < table->size(); ++j) { - auto obj = table->get_object(i); - - // Move the local uuid from the old column to the list - auto old_obj = old_table->get_object(j); - obj.get_list(new_uuid_col).add(old_obj.get(old_uuid_col)); - - // Check if we've already seen an object with the same id. If not, store - // this one and move on - std::string user_id = obj.get(id_col); - auto& existing = users[obj.get(id_col)]; - if (!existing.is_valid()) { - existing = obj; - ++i; - continue; - } - - // We have a second object for the same id, so we need to merge them. - // First we merge the state: if one is logged in and the other isn't, - // we'll use the logged-in state and tokens. If both are logged in, we'll - // use the more recent login. If one is logged out and the other is - // removed we'll use the logged out state. If both are logged out or - // both are removed then it doesn't matter which we pick. - using State = SyncUser::State; - auto state = State(obj.get(state_col)); - auto existing_state = State(existing.get(state_col)); - if (state == existing_state) { - if (state == State::LoggedIn) { - RealmJWT token_1(existing.get(c_sync_access_token)); - RealmJWT token_2(obj.get(c_sync_access_token)); - if (token_1.issued_at < token_2.issued_at) { - existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); - existing.set(c_sync_access_token, obj.get(c_sync_access_token)); - } - } - } - else if (state == State::LoggedIn || existing_state == State::Removed) { - existing.set(c_sync_state, int64_t(state)); - existing.set(c_sync_refresh_token, obj.get(c_sync_refresh_token)); - existing.set(c_sync_access_token, obj.get(c_sync_access_token)); - } - - // Next we merge the list properties (identities, legacy uuids, realm file paths) - { - auto dest = existing.get_linklist(c_sync_identities); - auto src = obj.get_linklist(c_sync_identities); - for (size_t i = 0, size = src.size(); i < size; ++i) { - if (dest.find_first(src.get(i)) == npos) { - dest.add(src.get(i)); - } - } - } - { - auto dest = existing.get_list(c_sync_legacy_uuids); - auto src = obj.get_list(c_sync_legacy_uuids); - for (size_t i = 0, size = src.size(); i < size; ++i) { - if (dest.find_first(src.get(i)) == npos) { - dest.add(src.get(i)); - } - } - } - { - auto dest = existing.get_set(c_sync_local_realm_paths); - auto src = obj.get_set(c_sync_local_realm_paths); - for (size_t i = 0, size = src.size(); i < size; ++i) { - dest.insert(src.get(i)); - } - } - - - // Finally we delete the duplicate object. We don't increment `i` as it's - // now the index of the object just after the one we're deleting. - obj.remove(); - } -} - -void migrate_to_v8(Realm&, Realm& realm) -{ - if (auto app_metadata_table = realm.read_group().get_table("class_AppMetadata")) { - realm.read_group().remove_table(app_metadata_table->get_key()); - } -} - -} // anonymous namespace - -// MARK: - Sync metadata manager - -SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key) -{ - constexpr uint64_t SCHEMA_VERSION = 7; - - if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) - throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); - - m_metadata_config.automatic_change_notifications = false; - m_metadata_config.path = path; - m_metadata_config.schema = make_schema(); - m_metadata_config.schema_version = SCHEMA_VERSION; - m_metadata_config.schema_mode = SchemaMode::Automatic; - m_metadata_config.scheduler = util::Scheduler::make_dummy(); - if (encryption_key) - m_metadata_config.encryption_key = std::move(*encryption_key); - m_metadata_config.automatically_handle_backlinks_in_migrations = true; - m_metadata_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, - Schema&) { - if (old_realm->schema_version() < 7) { - migrate_to_v7(*old_realm, *realm); - } - // note that the schema version has not yet been bumped to 8 - if (old_realm->schema_version() < 8) { - migrate_to_v8(*old_realm, *realm); - } - }; - - auto realm = open_realm(should_encrypt, encryption_key != none); - - // Get data about the (hardcoded) schemas - auto object_schema = realm->schema().find(c_sync_userMetadata); - m_user_schema = { - object_schema->persisted_properties[0].column_key, object_schema->persisted_properties[1].column_key, - object_schema->persisted_properties[2].column_key, object_schema->persisted_properties[3].column_key, - object_schema->persisted_properties[4].column_key, object_schema->persisted_properties[5].column_key, - object_schema->persisted_properties[6].column_key, object_schema->persisted_properties[7].column_key, - object_schema->persisted_properties[8].column_key}; - - object_schema = realm->schema().find(c_sync_fileActionMetadata); - m_file_action_schema = { - object_schema->persisted_properties[0].column_key, - object_schema->persisted_properties[1].column_key, - object_schema->persisted_properties[2].column_key, - }; -} - -void SyncMetadataManager::perform_launch_actions(SyncFileManager& file_manager) const -{ - auto realm = get_realm(); - - // Perform our "on next startup" actions such as deleting Realm files - // which we couldn't delete immediately due to them being in use - auto actions_table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - for (auto file_action : *actions_table) { - SyncFileActionMetadata md(m_file_action_schema, realm, file_action); - run_file_action(file_manager, md); - } - - // Delete any users marked for death. - auto users_table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - for (auto user : *users_table) { - if (user.get(m_user_schema.state_col) != int64_t(SyncUser::State::Removed)) - continue; - try { - SyncUserMetadata data(m_user_schema, realm, user); - file_manager.remove_user_realms(data.identity(), data.realm_file_paths()); - realm->begin_transaction(); - user.remove(); - realm->commit_transaction(); - } - catch (FileAccessError const&) { - continue; - } - } -} - -bool SyncMetadataManager::run_file_action(SyncFileManager& file_manager, SyncFileActionMetadata& md) const -{ - switch (md.action()) { - case SyncFileActionMetadata::Action::DeleteRealm: - // Delete all the files for the given Realm. - if (file_manager.remove_realm(md.original_name())) { - md.remove(); - return true; - } - break; - case SyncFileActionMetadata::Action::BackUpThenDeleteRealm: - // Copy the primary Realm file to the recovery dir, and then delete the Realm. - auto new_name = md.new_name(); - auto original_name = md.original_name(); - if (!util::File::exists(original_name)) { - // The Realm file doesn't exist anymore. - md.remove(); - return false; - } - if (new_name && !util::File::exists(*new_name) && - file_manager.copy_realm_file(original_name, *new_name)) { - // We successfully copied the Realm file to the recovery directory. - bool did_remove = file_manager.remove_realm(original_name); - // if the copy succeeded but not the delete, then running BackupThenDelete - // a second time would fail, so change this action to just delete the original file. - if (did_remove) { - md.remove(); - return true; - } - md.set_action(SyncFileActionMetadata::Action::DeleteRealm); - } - break; - } - return false; -} - -// Some of our string columns are nullable. They never should actually be -// null as we store "" rather than null when the value isn't present, but -// be safe and handle it anyway. -static std::string_view get_string(const Obj& obj, ColKey col) -{ - auto str = obj.get(col); - return str.is_null() ? "" : std::string_view(str); -} - -static bool is_valid_user(const SyncUserMetadata::Schema& schema, const Obj& obj) -{ - // This is overly cautious and merely checking the state should suffice, - // but because this is a persisted file that can be modified it's possible - // to get invalid combinations of data. - return obj && obj.get(schema.state_col) == int64_t(SyncUser::State::LoggedIn) && - RealmJWT::validate(get_string(obj, schema.access_token_col)) && - RealmJWT::validate(get_string(obj, schema.refresh_token_col)); -} - -std::vector SyncMetadataManager::all_logged_in_users() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - std::vector users; - users.reserve(table->size()); - for (auto obj : *table) { - if (is_valid_user(m_user_schema, obj)) { - users.emplace_back(m_user_schema, realm, obj); - } - } - return users; -} - -SyncUserMetadataResults SyncMetadataManager::all_unmarked_users() const -{ - return get_users(false); -} - -SyncUserMetadataResults SyncMetadataManager::all_users_marked_for_removal() const -{ - return get_users(true); -} - -SyncUserMetadataResults SyncMetadataManager::get_users(bool marked) const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query; - if (marked) { - query = table->where().equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); - } - else { - query = table->where().not_equal(m_user_schema.state_col, int64_t(SyncUser::State::Removed)); - } - return SyncUserMetadataResults(Results(realm, std::move(query)), m_user_schema); -} - -util::Optional SyncMetadataManager::get_current_user_identity() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - if (!table->is_empty()) { - auto first = table->begin(); - return util::Optional(first->get(c_sync_current_user_identity)); - } - - return util::Optional(); -} - -SyncFileActionMetadataResults SyncMetadataManager::all_pending_actions() const -{ - auto realm = get_realm(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - return SyncFileActionMetadataResults(Results(realm, table), m_file_action_schema); -} - -void SyncMetadataManager::set_current_user_identity(const std::string& identity) -{ - auto realm = get_realm(); - - realm->begin_transaction(); - - TableRef currentUserIdentityTable = - ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - Obj currentUserIdentityObj; - if (currentUserIdentityTable->is_empty()) - currentUserIdentityObj = currentUserIdentityTable->create_object(); - else - currentUserIdentityObj = *currentUserIdentityTable->begin(); - - currentUserIdentityObj.set(c_sync_current_user_identity, identity); - - realm->commit_transaction(); -} - -util::Optional SyncMetadataManager::get_or_make_user_metadata(const std::string& identity, - bool make_if_absent) const -{ - auto realm = get_realm(); - auto& schema = m_user_schema; - - // Retrieve or create the row for this object. - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_userMetadata); - Query query = table->where().equal(schema.identity_col, StringData(identity)); - Results results(realm, std::move(query)); - REALM_ASSERT_DEBUG(results.size() < 2); - auto obj = results.first(); - - if (!obj) { - if (!make_if_absent) - return none; - - realm->begin_transaction(); - // Check the results again. - obj = results.first(); - } - if (!obj) { - // Because "making this user" is our last action, set this new user as the current user - TableRef currentUserIdentityTable = - ObjectStore::table_for_object_type(realm->read_group(), c_sync_current_user_identity); - - Obj currentUserIdentityObj; - if (currentUserIdentityTable->is_empty()) - currentUserIdentityObj = currentUserIdentityTable->create_object(); - else - currentUserIdentityObj = *currentUserIdentityTable->begin(); - - obj = table->create_object(); - - currentUserIdentityObj.set(c_sync_current_user_identity, identity); - - obj->set(schema.identity_col, identity); - obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); - realm->commit_transaction(); - return SyncUserMetadata(schema, std::move(realm), *obj); - } - - // Got an existing user. - if (obj->get(schema.state_col) == int64_t(SyncUser::State::Removed)) { - // User is dead. Revive or return none. - if (!make_if_absent) { - return none; - } - - if (!realm->is_in_transaction()) - realm->begin_transaction(); - obj->set(schema.state_col, (int64_t)SyncUser::State::LoggedIn); - realm->commit_transaction(); - } - - return SyncUserMetadata(schema, std::move(realm), std::move(*obj)); -} - -void SyncMetadataManager::make_file_action_metadata(StringData original_name, SyncFileActionMetadata::Action action, - StringData new_name) const -{ - auto realm = get_realm(); - realm->begin_transaction(); - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - - auto& schema = m_file_action_schema; - Obj obj = table->create_object_with_primary_key(original_name); - - obj.set(schema.idx_new_name, new_name); - obj.set(schema.idx_action, static_cast(action)); - realm->commit_transaction(); -} - -util::Optional SyncMetadataManager::get_file_action_metadata(StringData original_name) const -{ - auto realm = get_realm(); - auto& schema = m_file_action_schema; - TableRef table = ObjectStore::table_for_object_type(realm->read_group(), c_sync_fileActionMetadata); - auto row_idx = table->find_first_string(schema.idx_original_name, original_name); - if (!row_idx) - return none; - - return SyncFileActionMetadata(std::move(schema), std::move(realm), table->get_object(row_idx)); -} - -bool SyncMetadataManager::perform_file_actions(SyncFileManager& file_manager, StringData path) const -{ - if (auto md = get_file_action_metadata(path)) { - return run_file_action(file_manager, *md); - } - return false; -} - -std::shared_ptr SyncMetadataManager::get_realm() const -{ - auto realm = Realm::get_shared_realm(m_metadata_config); - realm->refresh(); - return realm; -} - -std::shared_ptr SyncMetadataManager::try_get_realm() const -{ - try { - return get_realm(); - } - catch (const InvalidDatabase&) { - return nullptr; - } -} - -std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool caller_supplied_key) -{ - if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { - if (auto realm = try_get_realm()) - return realm; - - // Encryption key changed, so delete the existing metadata realm and - // recreate it - util::File::remove(m_metadata_config.path); - return get_realm(); - } - -#if REALM_PLATFORM_APPLE - // This logic is all a giant race condition once we have multi-process sync. - // Wrapping it all (including the keychain accesses) in DB::call_with_lock() - // might suffice. - - // First try to open the Realm with a key already stored in the keychain. - // This works for both the case where everything is sensible and valid and - // when we have a key but no metadata Realm. - auto key = keychain::get_existing_metadata_realm_key(); - if (key) { - m_metadata_config.encryption_key = *key; - if (auto realm = try_get_realm()) - return realm; - } - - // If we have an existing file and either no key or the key didn't work to - // decrypt it, then we might have an unencrypted metadata Realm resulting - // from a previous run being unable to access the keychain. - if (util::File::exists(m_metadata_config.path)) { - m_metadata_config.encryption_key.clear(); - if (auto realm = try_get_realm()) - return realm; - - // We weren't able to open the existing file with either the stored key - // or no key, so just delete it. - util::File::remove(m_metadata_config.path); - } - - // We now have no metadata Realm. If we don't have an existing stored key, - // try to create and store a new one. This might fail, in which case we - // just create an unencrypted Realm file. - if (!key) - key = keychain::create_new_metadata_realm_key(); - if (key) - m_metadata_config.encryption_key = std::move(*key); - return get_realm(); -#else // REALM_PLATFORM_APPLE - REALM_UNREACHABLE(); -#endif // REALM_PLATFORM_APPLE -} - -// MARK: - Sync user metadata - -SyncUserMetadata::SyncUserMetadata(Schema schema, SharedRealm realm, const Obj& obj) - : m_realm(std::move(realm)) - , m_schema(std::move(schema)) - , m_obj(obj) -{ -} - -std::string SyncUserMetadata::identity() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.identity_col); -} - -SyncUser::State SyncUserMetadata::state() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return SyncUser::State(m_obj.get(m_schema.state_col)); -} - -std::vector SyncUserMetadata::legacy_identities() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - std::vector uuids; - auto list = m_obj.get_list(m_schema.legacy_uuids_col); - for (size_t i = 0, size = list.size(); i < size; ++i) { - uuids.push_back(list.get(i)); - } - return uuids; -} - -std::string SyncUserMetadata::refresh_token() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.refresh_token_col); - return result.is_null() ? "" : std::string(result); -} - -std::string SyncUserMetadata::access_token() const -{ - REALM_ASSERT(m_realm); - StringData result = m_obj.get(m_schema.access_token_col); - return result.is_null() ? "" : std::string(result); -} - -std::string SyncUserMetadata::device_id() const -{ - REALM_ASSERT(m_realm); - StringData result = m_obj.get(m_schema.device_id_col); - return result.is_null() ? "" : std::string(result); -} - -std::vector SyncUserMetadata::identities() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - auto linklist = m_obj.get_linklist(m_schema.identities_col); - - std::vector identities; - for (size_t i = 0; i < linklist.size(); i++) { - auto obj = linklist.get_object(i); - identities.emplace_back(obj.get(c_sync_user_id), obj.get(c_sync_provider_type)); - } - - return identities; -} - -SyncUserProfile SyncUserMetadata::profile() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.profile_dump_col); - if (result.size() == 0) { - return SyncUserProfile(); - } - return SyncUserProfile(static_cast(bson::parse(std::string_view(result)))); -} - -void SyncUserMetadata::set_refresh_token(const std::string& refresh_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.refresh_token_col, refresh_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_state(SyncUser::State state) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.state_col, (int64_t)state); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.state_col, static_cast(state)); - m_obj.set(m_schema.access_token_col, access_token); - m_obj.set(m_schema.refresh_token_col, refresh_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_identities(std::vector identities) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - - auto link_list = m_obj.get_linklist(m_schema.identities_col); - auto identities_table = link_list.get_target_table(); - auto col_user_id = identities_table->get_column_key(c_sync_user_id); - auto col_provider_type = identities_table->get_column_key(c_sync_provider_type); - link_list.clear(); - - for (auto& ident : identities) { - auto obj = link_list.create_and_insert_linked_object(link_list.size()); - obj.set(col_user_id, ident.id); - obj.set(col_provider_type, ident.provider_type); - } - - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_access_token(const std::string& user_token) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.access_token_col, user_token); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_device_id(const std::string& device_id) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.device_id_col, device_id); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_legacy_identities(const std::vector& uuids) -{ - m_realm->begin_transaction(); - auto list = m_obj.get_list(m_schema.legacy_uuids_col); - list.clear(); - for (auto& uuid : uuids) - list.add(uuid); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::set_user_profile(const SyncUserProfile& profile) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - std::stringstream data; - data << profile.data(); - m_obj.set(m_schema.profile_dump_col, data.str()); - m_realm->commit_transaction(); -} - -std::vector SyncUserMetadata::realm_file_paths() const -{ - if (m_invalid) - return {}; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->refresh(); - Set paths = m_obj.get_set(m_schema.realm_file_paths_col); - return std::vector(paths.begin(), paths.end()); -} - -void SyncUserMetadata::add_realm_file_path(const std::string& path) -{ - if (m_invalid) - return; - - REALM_ASSERT_DEBUG(m_realm); - m_realm->begin_transaction(); - Set paths = m_obj.get_set(m_schema.realm_file_paths_col); - paths.insert(path); - m_realm->commit_transaction(); -} - -void SyncUserMetadata::remove() -{ - m_invalid = true; - m_realm->begin_transaction(); - m_obj.remove(); - m_realm->commit_transaction(); - m_realm = nullptr; -} - -// MARK: - File action metadata - -SyncFileActionMetadata::SyncFileActionMetadata(Schema schema, SharedRealm realm, const Obj& obj) - : m_realm(std::move(realm)) - , m_schema(std::move(schema)) - , m_obj(obj) -{ -} - -std::string SyncFileActionMetadata::original_name() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return m_obj.get(m_schema.idx_original_name); -} - -util::Optional SyncFileActionMetadata::new_name() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - StringData result = m_obj.get(m_schema.idx_new_name); - return result.is_null() ? util::none : util::make_optional(std::string(result)); -} - -SyncFileActionMetadata::Action SyncFileActionMetadata::action() const -{ - REALM_ASSERT(m_realm); - m_realm->refresh(); - return static_cast(m_obj.get(m_schema.idx_action)); -} - -void SyncFileActionMetadata::remove() -{ - REALM_ASSERT(m_realm); - m_realm->begin_transaction(); - m_obj.remove(); - m_realm->commit_transaction(); - m_realm = nullptr; -} - -void SyncFileActionMetadata::set_action(Action new_action) -{ - REALM_ASSERT(m_realm); - m_realm->begin_transaction(); - m_obj.set(m_schema.idx_action, static_cast(new_action)); - m_realm->commit_transaction(); -} diff --git a/src/realm/object-store/sync/impl/sync_metadata.hpp b/src/realm/object-store/sync/impl/sync_metadata.hpp deleted file mode 100644 index 4987c9e809f..00000000000 --- a/src/realm/object-store/sync/impl/sync_metadata.hpp +++ /dev/null @@ -1,241 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#ifndef REALM_OS_SYNC_METADATA_HPP -#define REALM_OS_SYNC_METADATA_HPP - -#include -#include -#include - -#include -#include -#include - -namespace realm { -class SyncFileManager; -class SyncMetadataManager; - -// A facade for a metadata Realm object representing a sync user. -class SyncUserMetadata { -public: - struct Schema { - // The server-supplied user_id for the user. Unique per App. - ColKey identity_col; - // Locally generated UUIDs for the user. These are tracked to be able - // to open pre-existing Realm files, but are no longer generated or - // used for anything else. - ColKey legacy_uuids_col; - // The cached refresh token for this user. - ColKey refresh_token_col; - // The cached access token for this user. - ColKey access_token_col; - // The identities for this user. - ColKey identities_col; - // The current state of this user. - ColKey state_col; - // The device id of this user. - ColKey device_id_col; - // Any additional profile attributes, formatted as a bson string. - ColKey profile_dump_col; - // The set of absolute file paths to Realms belonging to this user. - ColKey realm_file_paths_col; - }; - - // Cannot be set after creation. - std::string identity() const; - - std::vector legacy_identities() const; - // for testing purposes only - void set_legacy_identities(const std::vector&); - - std::vector identities() const; - void set_identities(std::vector); - - void set_state_and_tokens(SyncUser::State state, const std::string& access_token, - const std::string& refresh_token); - - std::string refresh_token() const; - void set_refresh_token(const std::string& token); - - std::string access_token() const; - void set_access_token(const std::string& token); - - std::string device_id() const; - void set_device_id(const std::string&); - - SyncUserProfile profile() const; - void set_user_profile(const SyncUserProfile&); - - std::vector realm_file_paths() const; - void add_realm_file_path(const std::string& path); - - void set_state(SyncUser::State); - - SyncUser::State state() const; - - void remove(); - - bool is_valid() const - { - return !m_invalid; - } - - // INTERNAL USE ONLY - SyncUserMetadata(Schema schema, SharedRealm realm, const Obj& obj); - -private: - bool m_invalid = false; - SharedRealm m_realm; - Schema m_schema; - Obj m_obj; -}; - -// A facade for a metadata Realm object representing a pending action to be carried out upon a specific file(s). -class SyncFileActionMetadata { -public: - struct Schema { - // The original path on disk of the file (generally, the main file for an on-disk Realm). - ColKey idx_original_name; - // A new path on disk for a file to be written to. Context-dependent. - ColKey idx_new_name; - // An enum describing the action to take. - ColKey idx_action; - }; - - enum class Action { - // The Realm files at the given directory will be deleted. - DeleteRealm, - // The Realm file will be copied to a 'recovery' directory, and the original Realm files will be deleted. - BackUpThenDeleteRealm - }; - - // The absolute path to the Realm file in question. - std::string original_name() const; - - // The meaning of this parameter depends on the `Action` specified. - // For `BackUpThenDeleteRealm`, it is the absolute path where the backup copy - // of the Realm file found at `original_name()` will be placed. - // For all other `Action`s, it is ignored. - util::Optional new_name() const; - - Action action() const; - void remove(); - void set_action(Action new_action); - - // INTERNAL USE ONLY - SyncFileActionMetadata(Schema schema, SharedRealm realm, const Obj& obj); - -private: - SharedRealm m_realm; - Schema m_schema; - Obj m_obj; -}; - -template -class SyncMetadataResults { -public: - size_t size() const - { - m_results.get_realm()->refresh(); - return m_results.size(); - } - - T get(size_t idx) const - { - m_results.get_realm()->refresh(); - auto row = m_results.get(idx); - return T(m_schema, m_results.get_realm(), row); - } - - SyncMetadataResults(Results results, typename T::Schema schema) - : m_schema(std::move(schema)) - , m_results(std::move(results)) - { - } - -private: - typename T::Schema m_schema; - mutable Results m_results; -}; -using SyncUserMetadataResults = SyncMetadataResults; -using SyncFileActionMetadataResults = SyncMetadataResults; - -// A facade for the application's metadata Realm. -class SyncMetadataManager { - friend class SyncUserMetadata; - friend class SyncFileActionMetadata; - -public: - std::vector all_logged_in_users() const; - - // Perform all pending file actions and delete any remaining data for removed users. - void perform_launch_actions(SyncFileManager& file_manager) const; - - // Return a Results object containing all users not marked for removal. - SyncUserMetadataResults all_unmarked_users() const; - - // Return a Results object containing all users marked for removal. It is the binding's responsibility to call - // `remove()` on each user to actually remove it from the database. (This is so that already-open Realm files can - // be safely cleaned up the next time the host is launched.) - SyncUserMetadataResults all_users_marked_for_removal() const; - - // Return a Results object containing all pending actions. - SyncFileActionMetadataResults all_pending_actions() const; - - // Retrieve or create user metadata. - // Note: if `make_is_absent` is true and the user has been marked for deletion, it will be unmarked. - util::Optional get_or_make_user_metadata(const std::string& identity, - bool make_if_absent = true) const; - - // Retrieve file action metadata. - util::Optional get_file_action_metadata(StringData path) const; - // Perform any stored file actions for the given path. - bool perform_file_actions(SyncFileManager& file_manager, StringData path) const; - - // Create file action metadata. - void make_file_action_metadata(StringData original_name, SyncFileActionMetadata::Action action, - StringData new_name = {}) const; - - util::Optional get_current_user_identity() const; - void set_current_user_identity(const std::string& identity); - - /// Construct the metadata manager. - /// - /// If the platform supports it, setting `should_encrypt` to `true` and not specifying an encryption key will make - /// the object store handle generating and persisting an encryption key for the metadata database. Otherwise, an - /// exception will be thrown. - SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key = none); - -private: - SyncUserMetadataResults get_users(bool marked) const; - Realm::Config m_metadata_config; - SyncUserMetadata::Schema m_user_schema; - SyncFileActionMetadata::Schema m_file_action_schema; - - std::shared_ptr get_realm() const; - std::shared_ptr try_get_realm() const; - std::shared_ptr open_realm(bool should_encrypt, bool caller_supplied_key); - - bool run_file_action(SyncFileManager& file_manager, SyncFileActionMetadata& md) const; -}; - -} // namespace realm - -#endif // REALM_OS_SYNC_METADATA_HPP diff --git a/src/realm/object-store/sync/mongo_client.hpp b/src/realm/object-store/sync/mongo_client.hpp index 7de73b7ca59..9cb419c59d5 100644 --- a/src/realm/object-store/sync/mongo_client.hpp +++ b/src/realm/object-store/sync/mongo_client.hpp @@ -22,12 +22,10 @@ #include #include -namespace realm { -class SyncUser; - -namespace app { +namespace realm::app { class AppServiceClient; class MongoDatabase; +class User; /// A client responsible for communication with a remote MongoDB database. class MongoClient { @@ -47,21 +45,19 @@ class MongoClient { MongoDatabase db(const std::string& name); private: - friend ::realm::SyncUser; - - MongoClient(std::shared_ptr user, std::shared_ptr service, std::string service_name) + friend class User; + MongoClient(std::shared_ptr user, std::shared_ptr service, std::string service_name) : m_user(std::move(user)) , m_service(std::move(service)) , m_service_name(std::move(service_name)) { } - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; std::string m_service_name; }; -} // namespace app -} // namespace realm +} // namespace realm::app #endif /* mongo_client_hpp */ diff --git a/src/realm/object-store/sync/mongo_collection.cpp b/src/realm/object-store/sync/mongo_collection.cpp index ea22e824b0a..5cdd6f8d457 100644 --- a/src/realm/object-store/sync/mongo_collection.cpp +++ b/src/realm/object-store/sync/mongo_collection.cpp @@ -101,8 +101,8 @@ ResponseHandler> get_document_handler(ResponseHandler& user, - const std::shared_ptr& service, const std::string& service_name) + const std::shared_ptr& user, const std::shared_ptr& service, + const std::string& service_name) : m_name(name) , m_database_name(database_name) , m_base_operation_args({{"database", m_database_name}, {"collection", m_name}}) diff --git a/src/realm/object-store/sync/mongo_collection.hpp b/src/realm/object-store/sync/mongo_collection.hpp index bdf08f15628..9b35d1e5ca3 100644 --- a/src/realm/object-store/sync/mongo_collection.hpp +++ b/src/realm/object-store/sync/mongo_collection.hpp @@ -27,10 +27,10 @@ #include namespace realm { -class SyncUser; namespace app { class AppServiceClient; +class User; struct AppError; class MongoCollection { @@ -346,7 +346,7 @@ class MongoCollection { private: friend class MongoDatabase; - MongoCollection(const std::string& name, const std::string& database_name, const std::shared_ptr& user, + MongoCollection(const std::string& name, const std::string& database_name, const std::shared_ptr& user, const std::shared_ptr& service, const std::string& service_name); void call_function(const char* name, const bson::BsonDocument& arg, @@ -361,7 +361,7 @@ class MongoCollection { /// Returns a document of database name and collection name bson::BsonDocument m_base_operation_args; - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; diff --git a/src/realm/object-store/sync/mongo_database.cpp b/src/realm/object-store/sync/mongo_database.cpp index 14176d64d47..18e9cfb287d 100644 --- a/src/realm/object-store/sync/mongo_database.cpp +++ b/src/realm/object-store/sync/mongo_database.cpp @@ -19,8 +19,7 @@ #include #include -namespace realm { -namespace app { +namespace realm::app { MongoCollection MongoDatabase::collection(const std::string& collection_name) { @@ -32,5 +31,4 @@ MongoCollection MongoDatabase::operator[](const std::string& collection_name) return MongoCollection(collection_name, m_name, m_user, m_service, m_service_name); } -} // namespace app -} // namespace realm +} // namespace realm::app diff --git a/src/realm/object-store/sync/mongo_database.hpp b/src/realm/object-store/sync/mongo_database.hpp index 200c71f61ba..255aacacfa4 100644 --- a/src/realm/object-store/sync/mongo_database.hpp +++ b/src/realm/object-store/sync/mongo_database.hpp @@ -22,12 +22,10 @@ #include #include -namespace realm { -class SyncUser; -namespace app { - +namespace realm::app { class AppServiceClient; class MongoCollection; +class User; class MongoDatabase { public: @@ -54,7 +52,7 @@ class MongoDatabase { MongoCollection operator[](const std::string& collection_name); private: - MongoDatabase(std::string name, std::shared_ptr user, std::shared_ptr service, + MongoDatabase(std::string name, std::shared_ptr user, std::shared_ptr service, std::string service_name) : m_name(std::move(name)) , m_user(std::move(user)) @@ -66,12 +64,11 @@ class MongoDatabase { friend class MongoClient; std::string m_name; - std::shared_ptr m_user; + std::shared_ptr m_user; std::shared_ptr m_service; std::string m_service_name; }; -} // namespace app -} // namespace realm +} // namespace realm::app #endif /* REALM_OS_MONGO_DATABASE_HPP */ diff --git a/src/realm/object-store/sync/push_client.cpp b/src/realm/object-store/sync/push_client.cpp index c87c1160fb6..86c92271305 100644 --- a/src/realm/object-store/sync/push_client.cpp +++ b/src/realm/object-store/sync/push_client.cpp @@ -37,7 +37,7 @@ wrap_completion(util::UniqueFunction)>&& completio } } // anonymous namespace -void PushClient::register_device(const std::string& registration_token, const std::shared_ptr& sync_user, +void PushClient::register_device(const std::string& registration_token, const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion) { auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); @@ -49,7 +49,7 @@ void PushClient::register_device(const std::string& registration_token, const st wrap_completion(std::move(completion))); } -void PushClient::deregister_device(const std::shared_ptr& sync_user, +void PushClient::deregister_device(const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion) { auto push_route = util::format("/app/%1/push/providers/%2/registration", m_app_id, m_service_name); diff --git a/src/realm/object-store/sync/push_client.hpp b/src/realm/object-store/sync/push_client.hpp index 2a904255172..a933752a7d7 100644 --- a/src/realm/object-store/sync/push_client.hpp +++ b/src/realm/object-store/sync/push_client.hpp @@ -26,9 +26,9 @@ #include namespace realm { -class SyncUser; namespace app { class AuthRequestClient; +class User; struct AppError; class PushClient { @@ -53,7 +53,7 @@ class PushClient { /// @param registration_token GCM registration token for the device. /// @param sync_user The sync user requesting push registration. /// @param completion An error will be returned should something go wrong. - void register_device(const std::string& registration_token, const std::shared_ptr& sync_user, + void register_device(const std::string& registration_token, const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion); @@ -61,7 +61,7 @@ class PushClient { /// as it is linked to the user in MongoDB Realm Cloud. /// @param sync_user The sync user requesting push degistration. /// @param completion An error will be returned should something go wrong. - void deregister_device(const std::shared_ptr& sync_user, + void deregister_device(const std::shared_ptr& sync_user, util::UniqueFunction)>&& completion); private: diff --git a/src/realm/object-store/sync/subscribable.hpp b/src/realm/object-store/sync/subscribable.hpp index ff3d9b9b462..84b1e5c58d8 100644 --- a/src/realm/object-store/sync/subscribable.hpp +++ b/src/realm/object-store/sync/subscribable.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include diff --git a/src/realm/object-store/sync/sync_manager.cpp b/src/realm/object-store/sync/sync_manager.cpp index 24713d85c42..fcaa336ce8f 100644 --- a/src/realm/object-store/sync/sync_manager.cpp +++ b/src/realm/object-store/sync/sync_manager.cpp @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include #include @@ -43,68 +43,23 @@ SyncClientTimeouts::SyncClientTimeouts() { } -std::shared_ptr SyncManager::create(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id) +std::shared_ptr SyncManager::create(const SyncClientConfig& config) { - return std::make_shared(Private(), std::move(app), std::move(sync_route), config, app_id); + return std::make_shared(Private(), config); } -SyncManager::SyncManager(Private, std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id) +SyncManager::SyncManager(Private, const SyncClientConfig& config) : m_config(config) - , m_file_manager(std::make_unique(m_config.base_file_path, app_id)) - , m_sync_route(sync_route) - , m_app(app) - , m_app_id(app_id) { // create the initial logger - if the logger_factory is updated later, a new // logger will be created at that time. do_make_logger(); - - if (m_config.metadata_mode == MetadataMode::NoMetadata) { - return; - } - - bool encrypt = m_config.metadata_mode == MetadataMode::Encryption; - m_metadata_manager = std::make_unique(m_file_manager->metadata_path(), encrypt, - m_config.custom_encryption_key); - - m_metadata_manager->perform_launch_actions(*m_file_manager); - - // Load persisted users into the users map. - for (auto user : m_metadata_manager->all_logged_in_users()) { - m_users.push_back(std::make_shared(SyncUser::Private(), user, this)); - } -} - -bool SyncManager::immediately_run_file_actions(const std::string& realm_path) -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (m_metadata_manager) { - return m_metadata_manager->perform_file_actions(*m_file_manager, realm_path); - } - return false; } void SyncManager::tear_down_for_testing() { close_all_sessions(); - { - util::CheckedLockGuard lock(m_file_system_mutex); - m_metadata_manager = nullptr; - } - - { - // Destroy all the users. - util::CheckedLockGuard lock(m_user_mutex); - for (auto& user : m_users) { - user->detach_from_sync_manager(); - } - m_users.clear(); - m_current_user = nullptr; - } - { util::CheckedLockGuard lock(m_mutex); // Stop the client. This will abort any uploads that inactive sessions are waiting for. @@ -148,16 +103,9 @@ void SyncManager::tear_down_for_testing() { util::CheckedLockGuard lock(m_mutex); // Destroy the client now that we have no remaining sessions. - m_sync_client = nullptr; + m_sync_client.reset(); m_logger_ptr.reset(); - m_sync_route.reset(); - } - - { - util::CheckedLockGuard lock(m_file_system_mutex); - if (m_file_manager) - util::try_remove_dir_recursive(m_file_manager->base_path()); - m_file_manager = nullptr; + m_sync_route.clear(); } } @@ -192,6 +140,7 @@ void SyncManager::do_make_logger() else { m_logger_ptr = util::Logger::get_default_logger(); } + REALM_ASSERT(m_logger_ptr); } const std::shared_ptr& SyncManager::get_logger() const @@ -226,169 +175,6 @@ util::Logger::Level SyncManager::log_level() const noexcept return m_config.log_level; } -bool SyncManager::perform_metadata_update(util::FunctionRef update_function) const -{ - util::CheckedLockGuard lock(m_file_system_mutex); - if (!m_metadata_manager) { - return false; - } - update_function(*m_metadata_manager); - return true; -} - -std::shared_ptr SyncManager::get_user(const std::string& user_id, const std::string& refresh_token, - const std::string& access_token, const std::string& device_id) -{ - std::shared_ptr user; - { - util::CheckedLockGuard lock(m_user_mutex); - auto it = std::find_if(m_users.begin(), m_users.end(), [&](const auto& user) { - return user->identity() == user_id && user->state() != SyncUser::State::Removed; - }); - if (it == m_users.end()) { - // No existing user. - auto new_user = std::make_shared(SyncUser::Private(), refresh_token, user_id, access_token, - device_id, this); - m_users.emplace(m_users.begin(), new_user); - { - util::CheckedLockGuard lock(m_file_system_mutex); - // m_current_user is normally set very indirectly via the metadata manger - if (!m_metadata_manager) - m_current_user = new_user; - } - return new_user; - } - - // LoggedOut => LoggedIn - user = *it; - REALM_ASSERT(user->state() != SyncUser::State::Removed); - } - user->log_in(access_token, refresh_token); - return user; -} - -std::vector> SyncManager::all_users() -{ - util::CheckedLockGuard lock(m_user_mutex); - m_users.erase(std::remove_if(m_users.begin(), m_users.end(), - [](auto& user) { - bool should_remove = (user->state() == SyncUser::State::Removed); - if (should_remove) { - user->detach_from_sync_manager(); - } - return should_remove; - }), - m_users.end()); - return m_users; -} - -std::shared_ptr SyncManager::get_user_for_identity(std::string const& identity) const noexcept -{ - auto is_active_user = [identity](auto& el) { - return el->identity() == identity; - }; - auto it = std::find_if(m_users.begin(), m_users.end(), is_active_user); - return it == m_users.end() ? nullptr : *it; -} - -std::shared_ptr SyncManager::get_current_user() const -{ - util::CheckedLockGuard lock(m_user_mutex); - - if (m_current_user) - return m_current_user; - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (!m_metadata_manager) - return nullptr; - - auto cur_user_ident = m_metadata_manager->get_current_user_identity(); - return cur_user_ident ? get_user_for_identity(*cur_user_ident) : nullptr; -} - -void SyncManager::log_out_user(const SyncUser& user) -{ - util::CheckedLockGuard lock(m_user_mutex); - - // Move this user to the end of the vector - auto user_pos = std::partition(m_users.begin(), m_users.end(), [&](auto& u) { - return u.get() != &user; - }); - - auto active_user = std::find_if(m_users.begin(), user_pos, [](auto& u) { - return u->state() == SyncUser::State::LoggedIn; - }); - - util::CheckedLockGuard fs_lock(m_file_system_mutex); - bool was_active = m_current_user.get() == &user || - (m_metadata_manager && m_metadata_manager->get_current_user_identity() == user.identity()); - if (!was_active) - return; - - // Set the current active user to the next logged in user, or null if none - if (active_user != user_pos) { - m_current_user = *active_user; - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity((*active_user)->identity()); - } - else { - m_current_user = nullptr; - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(""); - } -} - -void SyncManager::set_current_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - - m_current_user = get_user_for_identity(user_id); - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (m_metadata_manager) - m_metadata_manager->set_current_user_identity(user_id); -} - -void SyncManager::remove_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - if (auto user = get_user_for_identity(user_id)) - user->invalidate(); -} - -void SyncManager::delete_user(const std::string& user_id) -{ - util::CheckedLockGuard lock(m_user_mutex); - // Avoid iterating over m_users twice by not calling `get_user_for_identity`. - auto it = std::find_if(m_users.begin(), m_users.end(), [&user_id](auto& user) { - return user->identity() == user_id; - }); - auto user = it == m_users.end() ? nullptr : *it; - - if (!user) - return; - - // Deletion should happen immediately, not when we do the cleanup - // task on next launch. - m_users.erase(it); - user->detach_from_sync_manager(); - - if (m_current_user && m_current_user->identity() == user->identity()) - m_current_user = nullptr; - - util::CheckedLockGuard fs_lock(m_file_system_mutex); - if (!m_metadata_manager) - return; - - auto users = m_metadata_manager->all_unmarked_users(); - for (size_t i = 0; i < users.size(); i++) { - auto metadata = users.get(i); - if (user->identity() == metadata.identity()) { - m_file_manager->remove_user_realms(metadata.identity(), metadata.realm_file_paths()); - metadata.remove(); - break; - } - } -} - SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS { // Grab the current sessions under a lock so we can shut them down. We have to @@ -404,13 +190,6 @@ SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS session->detach_from_sync_manager(); } - { - util::CheckedLockGuard lk(m_user_mutex); - for (auto& user : m_users) { - user->detach_from_sync_manager(); - } - } - { util::CheckedLockGuard lk(m_mutex); // Stop the client. This will abort any uploads that inactive sessions are waiting for. @@ -419,89 +198,26 @@ SyncManager::~SyncManager() NO_THREAD_SAFETY_ANALYSIS } } -std::shared_ptr SyncManager::get_existing_logged_in_user(const std::string& user_id) const -{ - util::CheckedLockGuard lock(m_user_mutex); - auto user = get_user_for_identity(user_id); - return user && user->state() == SyncUser::State::LoggedIn ? user : nullptr; -} - -struct UnsupportedBsonPartition : public std::logic_error { - UnsupportedBsonPartition(std::string msg) - : std::logic_error(msg) - { - } -}; - -static std::string string_from_partition(const std::string& partition) -{ - bson::Bson partition_value = bson::parse(partition); - switch (partition_value.type()) { - case bson::Bson::Type::Int32: - return util::format("i_%1", static_cast(partition_value)); - case bson::Bson::Type::Int64: - return util::format("l_%1", static_cast(partition_value)); - case bson::Bson::Type::String: - return util::format("s_%1", static_cast(partition_value)); - case bson::Bson::Type::ObjectId: - return util::format("o_%1", static_cast(partition_value).to_string()); - case bson::Bson::Type::Uuid: - return util::format("u_%1", static_cast(partition_value).to_string()); - case bson::Bson::Type::Null: - return "null"; - default: - throw UnsupportedBsonPartition(util::format("Unsupported partition key value: '%1'. Only int, string " - "UUID and ObjectId types are currently supported.", - partition_value.to_string())); - } -} - -std::string SyncManager::path_for_realm(const SyncConfig& config, util::Optional custom_file_name) const +std::vector> SyncManager::get_all_sessions() const { - auto user = config.user; - REALM_ASSERT(user); - std::string path; - { - util::CheckedLockGuard lock(m_file_system_mutex); - REALM_ASSERT(m_file_manager); - - // Attempt to make a nicer filename which will ease debugging when - // locating files in the filesystem. - auto file_name = [&]() -> std::string { - if (custom_file_name) { - return *custom_file_name; - } - if (config.flx_sync_requested) { - REALM_ASSERT_DEBUG(config.partition_value.empty()); - return "flx_sync_default"; - } - return string_from_partition(config.partition_value); - }(); - path = m_file_manager->realm_file_path(user->identity(), user->legacy_identities(), file_name, - config.partition_value); + util::CheckedLockGuard lock(m_session_mutex); + std::vector> sessions; + for (auto& [_, session] : m_sessions) { + if (auto external_reference = session->existing_external_reference()) + sessions.push_back(std::move(external_reference)); } - // Report the use of a Realm for this user, so the metadata can track it for clean up. - perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(user->identity()); - metadata->add_realm_file_path(path); - }); - return path; -} - -std::string SyncManager::recovery_directory_path(util::Optional const& custom_dir_name) const -{ - util::CheckedLockGuard lock(m_file_system_mutex); - REALM_ASSERT(m_file_manager); - return m_file_manager->recovery_directory_path(custom_dir_name); + return sessions; } -std::vector> SyncManager::get_all_sessions() const +std::vector> SyncManager::get_all_sessions_for(const SyncUser& user) const { util::CheckedLockGuard lock(m_session_mutex); std::vector> sessions; for (auto& [_, session] : m_sessions) { - if (auto external_reference = session->existing_external_reference()) - sessions.push_back(std::move(external_reference)); + if (session->user().get() == &user) { + if (auto external_reference = session->existing_external_reference()) + sessions.push_back(std::move(external_reference)); + } } return sessions; } @@ -544,7 +260,6 @@ std::shared_ptr SyncManager::get_session(std::shared_ptr db, co util::CheckedUniqueLock lock(m_session_mutex); if (auto session = get_existing_session_locked(path)) { - config.sync_config->user->register_session(session); return session->external_reference(); } @@ -553,15 +268,7 @@ std::shared_ptr SyncManager::get_session(std::shared_ptr db, co // Create the external reference immediately to ensure that the session will become // inactive if an exception is thrown in the following code. - auto external_reference = shared_session->external_reference(); - // unlocking m_session_mutex here prevents a deadlock for synchronous network - // transports such as the unit test suite, in the case where the log in request is - // denied by the server: Active -> WaitingForAccessToken -> handle_refresh(401 - // error) -> user.log_out() -> unregister_session (locks m_session_mutex again) - lock.unlock(); - config.sync_config->user->register_session(std::move(shared_session)); - - return external_reference; + return shared_session->external_reference(); } bool SyncManager::has_existing_sessions() @@ -623,6 +330,30 @@ void SyncManager::unregister_session(const std::string& path) lock.unlock(); } +void SyncManager::update_sessions_for(SyncUser& user, SyncUser::State old_state, SyncUser::State new_state, + std::string_view new_access_token) +{ + bool should_revive = old_state != SyncUser::State::LoggedIn && new_state == SyncUser::State::LoggedIn; + bool should_stop = old_state == SyncUser::State::LoggedIn && new_state != SyncUser::State::LoggedIn; + + auto sessions = get_all_sessions_for(user); + if (new_access_token.size()) { + for (auto& session : sessions) { + session->update_access_token(new_access_token); + } + } + else if (should_revive) { + for (auto& session : sessions) { + session->revive_if_needed(); + } + } + else if (should_stop) { + for (auto& session : sessions) { + session->force_close(); + } + } +} + void SyncManager::set_session_multiplexing(bool allowed) { util::CheckedLockGuard lock(m_mutex); @@ -646,12 +377,13 @@ SyncClient& SyncManager::get_sync_client() const std::unique_ptr SyncManager::create_sync_client() const { + REALM_ASSERT(m_logger_ptr); return std::make_unique(m_logger_ptr, m_config, weak_from_this()); } void SyncManager::close_all_sessions() { - // log_out() will call unregister_session(), which requires m_session_mutex, + // force_close() will call unregister_session(), which requires m_session_mutex, // so we need to iterate over them without holding the lock. decltype(m_sessions) sessions; { diff --git a/src/realm/object-store/sync/sync_manager.hpp b/src/realm/object-store/sync/sync_manager.hpp index 12e345a93f6..d5a335c96e6 100644 --- a/src/realm/object-store/sync/sync_manager.hpp +++ b/src/realm/object-store/sync/sync_manager.hpp @@ -19,17 +19,10 @@ #ifndef REALM_OS_SYNC_MANAGER_HPP #define REALM_OS_SYNC_MANAGER_HPP -#include - +#include +#include #include -#include -#include -#include -#include -#include - -#include -#include + #include class TestAppSession; @@ -39,87 +32,21 @@ namespace realm { class DB; struct SyncConfig; +struct RealmConfig; class SyncSession; -class SyncUser; -class SyncFileManager; -class SyncMetadataManager; -class SyncFileActionMetadata; namespace _impl { struct SyncClient; } -namespace app { -class App; -} - -struct SyncClientTimeouts { - SyncClientTimeouts(); - // See sync::Client::Config for the meaning of these fields. - uint64_t connect_timeout; - uint64_t connection_linger_time; - uint64_t ping_keepalive_period; - uint64_t pong_keepalive_timeout; - uint64_t fast_reconnect_limit; -}; - -struct SyncClientConfig { - enum class MetadataMode { - NoEncryption, // Enable metadata, but disable encryption. - Encryption, // Enable metadata, and use encryption (automatic if possible). - NoMetadata, // Disable metadata. - }; - - std::string base_file_path; - MetadataMode metadata_mode = MetadataMode::Encryption; - util::Optional> custom_encryption_key; - - using LoggerFactory = std::function(util::Logger::Level)>; - LoggerFactory logger_factory; - util::Logger::Level log_level = util::Logger::Level::info; - ReconnectMode reconnect_mode = ReconnectMode::normal; // For internal sync-client testing only! -#if REALM_DISABLE_SYNC_MULTIPLEXING - bool multiplex_sessions = false; -#else - bool multiplex_sessions = true; -#endif - - // The SyncSocket instance used by the Sync Client for event synchronization - // and creating WebSockets. If not provided the default implementation will be used. - std::shared_ptr socket_provider; - - // Optional thread observer for event loop thread events in the default SyncSocketProvider - // implementation. It is not used for custom SyncSocketProvider implementations. - std::shared_ptr default_socket_provider_thread_observer; - - // {@ - // Optional information about the binding/application that is sent as part of the User-Agent - // when establishing a connection to the server. These values are only used by the default - // SyncSocket implementation. Custom SyncSocket implementations must update the User-Agent - // directly, if supported by the platform APIs. - std::string user_agent_binding_info; - std::string user_agent_application_info; - // @} - - SyncClientTimeouts timeouts; -}; - -class SyncManager : public std::enable_shared_from_this { +class SyncManager : private std::enable_shared_from_this { struct Private {}; public: - using MetadataMode = SyncClientConfig::MetadataMode; - - // Immediately run file actions for a single Realm at a given original path. - // Returns whether or not a file action was successfully executed for the specified Realm. - // Preconditions: all references to the Realm at the given path must have already been invalidated. - // The metadata and file management subsystems must also have already been configured. - bool immediately_run_file_actions(const std::string& original_name) REQUIRES(!m_file_system_mutex); - // Enables/disables using a single connection for all sync sessions for each host/port/user rather // than one per session. - // This must be called before any sync sessions are created, cannot be - // disabled afterwards, and currently is incompatible with automatic failover. + // This must be called before any sync sessions are created and cannot be + // disabled afterwards. void set_session_multiplexing(bool allowed) REQUIRES(!m_mutex); // Destroys the sync manager, terminates all sessions created by it, and stops its SyncClient. @@ -154,6 +81,8 @@ class SyncManager : public std::enable_shared_from_this { util::Logger::Level log_level() const noexcept REQUIRES(!m_mutex); std::vector> get_all_sessions() const REQUIRES(!m_session_mutex); + std::vector> get_all_sessions_for(const SyncUser& user) const + REQUIRES(!m_session_mutex); std::shared_ptr get_session(std::shared_ptr db, const RealmConfig& config) REQUIRES(!m_mutex, !m_session_mutex); std::shared_ptr get_existing_session(const std::string& path) const REQUIRES(!m_session_mutex); @@ -171,57 +100,20 @@ class SyncManager : public std::enable_shared_from_this { // makes it possible to guarantee that all sessions have, in fact, been closed. void wait_for_sessions_to_terminate() REQUIRES(!m_mutex); - // If the metadata manager is configured, perform an update. Returns `true` if the code was run. - bool perform_metadata_update(util::FunctionRef update_function) const - REQUIRES(!m_file_system_mutex); - - // Get a sync user for a given identity, or create one if none exists yet, and set its token. - // If a logged-out user exists, it will marked as logged back in. - std::shared_ptr get_user(const std::string& user_id, const std::string& refresh_token, - const std::string& access_token, const std::string& device_id) - REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Get an existing user for a given identifier, if one exists and is logged in. - std::shared_ptr get_existing_logged_in_user(const std::string& user_id) const REQUIRES(!m_user_mutex); - - // Get all the users that are logged in and not errored out. - std::vector> all_users() REQUIRES(!m_user_mutex); - - // Gets the currently active user. - std::shared_ptr get_current_user() const REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Log out a given user - void log_out_user(const SyncUser& user) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Sets the currently active user. - void set_current_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Removes a user - void remove_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Permanently deletes a user. - void delete_user(const std::string& user_id) REQUIRES(!m_user_mutex, !m_file_system_mutex); - - // Get the default path for a Realm for the given configuration. - // The default value is `///.realm`. - // If the file cannot be created at this location, for example due to path length restrictions, - // this function may pass back `/.realm` - std::string path_for_realm(const SyncConfig& config, util::Optional custom_file_name = none) const - REQUIRES(!m_file_system_mutex); - - // Get the path of the recovery directory for backed-up or recovered Realms. - std::string recovery_directory_path(util::Optional const& custom_dir_name = none) const - REQUIRES(!m_file_system_mutex); - // DO NOT CALL OUTSIDE OF TESTING CODE. // Forcibly close all remaining sync sessions, stop the sync client, and // discard all state. The SyncManager must never be used again after this - // function has been called. - void tear_down_for_testing() REQUIRES(!m_mutex, !m_file_system_mutex, !m_user_mutex, !m_session_mutex); + // function has been called (note: not after it has returned). + void tear_down_for_testing() REQUIRES(!m_mutex, !m_session_mutex); // Immediately closes any open sync sessions for this sync manager void close_all_sessions() REQUIRES(!m_mutex, !m_session_mutex); + // Update all sessions for a given user following a state change for that + // user (and optionally a new access token) + void update_sessions_for(SyncUser& user, SyncUser::State old_state, SyncUser::State new_state, + std::string_view new_access_token) REQUIRES(!m_mutex, !m_session_mutex); + // Used by App to update the sync route any time the location info has been refreshed. // m_sync_route starts out as unset when the SyncManager is created or configured. // It will be updated to a valid value upon the first App AppServices HTTP request or @@ -238,18 +130,7 @@ class SyncManager : public std::enable_shared_from_this { const std::optional sync_route() const REQUIRES(!m_mutex) { util::CheckedLockGuard lock(m_mutex); - return m_sync_route; - } - - std::weak_ptr app() const REQUIRES(!m_mutex) - { - util::CheckedLockGuard lock(m_mutex); - return m_app; - } - - const std::string& app_id() const - { - return m_app_id; + return m_sync_route.empty() ? std::nullopt : std::make_optional(m_sync_route); } SyncClientConfig config() const REQUIRES(!m_mutex) @@ -267,10 +148,8 @@ class SyncManager : public std::enable_shared_from_this { static void voluntary_disconnect_all_connections(SyncManager&); }; - static std::shared_ptr create(std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id); - SyncManager(Private, std::shared_ptr app, std::optional sync_route, - const SyncClientConfig& config, const std::string& app_id); + static std::shared_ptr create(const SyncClientConfig& config); + SyncManager(Private, const SyncClientConfig& config); private: friend class app::App; @@ -279,6 +158,18 @@ class SyncManager : public std::enable_shared_from_this { friend class ::TestSyncManager; friend class ::TestAppSession; + util::CheckedMutex m_mutex; + mutable std::unique_ptr<_impl::SyncClient> m_sync_client GUARDED_BY(m_mutex); + SyncClientConfig m_config GUARDED_BY(m_mutex); + std::shared_ptr m_logger_ptr GUARDED_BY(m_mutex); + std::string m_sync_route GUARDED_BY(m_mutex); + + // Map of sessions by path name. + // Sessions remove themselves from this map by calling `unregister_session` once they're + // inactive and have performed any necessary cleanup work. + util::CheckedMutex m_session_mutex; + std::unordered_map> m_sessions GUARDED_BY(m_session_mutex); + // Stop tracking the session for the given path if it is inactive. // No-op if the session is either still active or in the active sessions list // due to someone holding a strong reference to it. @@ -289,51 +180,14 @@ class SyncManager : public std::enable_shared_from_this { std::shared_ptr get_existing_session_locked(const std::string& path) const REQUIRES(m_session_mutex); - std::shared_ptr get_user_for_identity(std::string const& identity) const noexcept - REQUIRES(m_user_mutex); - - util::CheckedMutex m_mutex; - void init_metadata(SyncClientConfig config, const std::string& app_id); // internally create a new logger - used by configure() and set_logger_factory() void do_make_logger() REQUIRES(m_mutex); - // Protects m_users - util::CheckedMutex m_user_mutex; - - // A vector of all SyncUser objects. - std::vector> m_users GUARDED_BY(m_user_mutex); - std::shared_ptr m_current_user GUARDED_BY(m_user_mutex); - - mutable std::unique_ptr<_impl::SyncClient> m_sync_client GUARDED_BY(m_mutex); - - SyncClientConfig m_config GUARDED_BY(m_mutex); - mutable std::shared_ptr m_logger_ptr GUARDED_BY(m_mutex); - - // Protects m_file_manager and m_metadata_manager - util::CheckedMutex m_file_system_mutex; - std::unique_ptr m_file_manager GUARDED_BY(m_file_system_mutex); - std::unique_ptr m_metadata_manager GUARDED_BY(m_file_system_mutex); - - // Protects m_sessions - util::CheckedMutex m_session_mutex; - - // Map of sessions by path name. - // Sessions remove themselves from this map by calling `unregister_session` once they're - // inactive and have performed any necessary cleanup work. - std::unordered_map> m_sessions GUARDED_BY(m_session_mutex); - // Internal method returning `true` if the SyncManager still contains sessions not yet fully closed. // Callers of this method should hold the `m_session_mutex` themselves. bool do_has_existing_sessions() REQUIRES(m_session_mutex); - - // The sync route URL to connect to the server. This can be initially empty, but should not - // be cleared once it has been set to a value, except by `tear_down_for_testing()`. - std::optional m_sync_route GUARDED_BY(m_mutex); - - std::weak_ptr m_app GUARDED_BY(m_mutex); - const std::string m_app_id; }; } // namespace realm diff --git a/src/realm/object-store/sync/sync_session.cpp b/src/realm/object-store/sync/sync_session.cpp index c20ec940633..4911adedd19 100644 --- a/src/realm/object-store/sync/sync_session.cpp +++ b/src/realm/object-store/sync/sync_session.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -269,7 +269,7 @@ void SyncSession::handle_bad_auth(const std::shared_ptr& user, Status cancel_pending_waits(std::move(lock), status); } if (user) { - user->log_out(); + user->request_log_out(); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -315,10 +315,7 @@ SyncSession::handle_refresh(const std::shared_ptr& session, bool re session->cancel_pending_waits(std::move(lock), refresh_error); } else if (error) { - if (error->code() == ErrorCodes::ClientAppDeallocated) { - return; // this response came in after the app shut down, ignore it - } - else if (!session->get_sync_route()) { + if (!session->get_sync_route()) { // If the sync route is empty at this point, it means the forced location update // failed while trying to start a sync session with a cached user and no other // AppServices HTTP requests have been performed since the App was created. @@ -425,11 +422,10 @@ SyncSession::SyncSession(Private, SyncClient& client, std::shared_ptr db, co } } -std::shared_ptr SyncSession::sync_manager() const +SyncManager* SyncSession::sync_manager() const { util::CheckedLockGuard lk(m_state_mutex); - REALM_ASSERT(m_sync_manager); - return m_sync_manager->shared_from_this(); + return m_sync_manager; } void SyncSession::detach_from_sync_manager() @@ -443,21 +439,15 @@ void SyncSession::update_error_and_mark_file_for_deletion(SyncError& error, Shou { util::CheckedLockGuard config_lock(m_config_mutex); // Add a SyncFileActionMetadata marking the Realm as needing to be deleted. - std::string recovery_path; auto original_path = path(); error.user_info[SyncError::c_original_file_path_key] = original_path; + using Action = SyncFileAction; + auto action = should_backup == ShouldBackup::yes ? Action::BackUpThenDeleteRealm : Action::DeleteRealm; + std::string recovery_path = m_config.sync_config->user->create_file_action( + action, original_path, m_config.sync_config->recovery_directory); if (should_backup == ShouldBackup::yes) { - recovery_path = util::reserve_unique_file_name( - m_sync_manager->recovery_directory_path(m_config.sync_config->recovery_directory), - util::create_timestamped_template("recovered_realm")); error.user_info[SyncError::c_recovery_file_path_key] = recovery_path; } - using Action = SyncFileActionMetadata::Action; - auto action = should_backup == ShouldBackup::yes ? Action::BackUpThenDeleteRealm : Action::DeleteRealm; - m_sync_manager->perform_metadata_update([action, original_path = std::move(original_path), - recovery_path = std::move(recovery_path)](const auto& manager) { - manager.make_file_action_metadata(original_path, action, recovery_path); - }); } void SyncSession::download_fresh_realm(sync::ProtocolErrorInfo::Action server_requests_action) @@ -762,16 +752,14 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) return; case sync::ProtocolErrorInfo::Action::RefreshUser: if (auto u = user()) { - u->refresh_custom_data(false, handle_refresh(shared_from_this(), false)); - return; + u->request_access_token(handle_refresh(shared_from_this(), false)); } - break; + return; case sync::ProtocolErrorInfo::Action::RefreshLocation: if (auto u = user()) { - u->refresh_custom_data(true, handle_refresh(shared_from_this(), true)); - return; + u->request_refresh_location(handle_refresh(shared_from_this(), true)); } - break; + return; case sync::ProtocolErrorInfo::Action::LogOutUser: next_state = NextStateAfterError::inactive; log_out_user = true; @@ -827,7 +815,7 @@ void SyncSession::handle_error(sync::SessionErrorInfo error) if (log_out_user) { if (auto u = user()) - u->log_out(); + u->request_log_out(); } if (auto error_handler = config(&SyncConfig::error_handler)) { @@ -932,7 +920,7 @@ void SyncSession::create_sync_session() sync::Session::Config session_config; session_config.signed_user_token = sync_config.user->access_token(); - session_config.user_id = sync_config.user->identity(); + session_config.user_id = sync_config.user->user_id(); session_config.realm_identifier = sync_config.partition_value; session_config.verify_servers_ssl_certificate = sync_config.client_validate_ssl; session_config.ssl_trust_certificate_path = sync_config.ssl_trust_certificate_path; @@ -1215,23 +1203,30 @@ void SyncSession::shutdown_and_wait() m_client.wait_for_session_terminations(); } -void SyncSession::update_access_token(const std::string& signed_token) +void SyncSession::update_access_token(std::string_view signed_token) { util::CheckedUniqueLock lock(m_state_mutex); - // We don't expect there to be a session when waiting for access token, but if there is, refresh its token. - // If not, the latest token will be seeded from SyncUser::access_token() on session creation. - if (m_session) { - m_session->refresh(signed_token); - } - if (m_state == State::WaitingForAccessToken) { - become_active(); + switch (m_state) { + case State::Active: + m_session->refresh(signed_token); + break; + case State::WaitingForAccessToken: + become_active(); + break; + case State::Paused: + // token will be pulled from user when the session is unpaused + return; + case State::Dying: + case State::Inactive: + do_revive(std::move(lock)); + break; } } void SyncSession::initiate_access_token_refresh() { if (auto session_user = user()) { - session_user->refresh_custom_data(handle_refresh(shared_from_this(), false)); + session_user->request_access_token(handle_refresh(shared_from_this(), false)); } } diff --git a/src/realm/object-store/sync/sync_session.hpp b/src/realm/object-store/sync/sync_session.hpp index ceed4dac480..64d50c7ade0 100644 --- a/src/realm/object-store/sync/sync_session.hpp +++ b/src/realm/object-store/sync/sync_session.hpp @@ -93,9 +93,7 @@ class SyncProgressNotifier { // Will be `none` until we've received the initial notification from sync. Note that this // happens only once ever during the lifetime of a given `SyncSession`, since these values are // expected to semi-monotonically increase, and a lower-bounds estimate is still useful in the - // event more up-to-date information isn't yet available. FIXME: If we support transparent - // client reset in the future, we might need to reset the progress state variables if the Realm - // is rolled back. + // event more up-to-date information isn't yet available. util::Optional m_current_progress; std::unordered_map m_packages; @@ -232,7 +230,7 @@ class SyncSession : public std::enable_shared_from_this { // The access token needs to periodically be refreshed and this is how to // let the sync session know to update it's internal copy. - void update_access_token(const std::string& signed_token) REQUIRES(!m_state_mutex, !m_config_mutex); + void update_access_token(std::string_view signed_token) REQUIRES(!m_state_mutex, !m_config_mutex); // Request an updated access token from this session's sync user. void initiate_access_token_refresh() REQUIRES(!m_config_mutex); @@ -360,11 +358,11 @@ class SyncSession : public std::enable_shared_from_this { const RealmConfig& config, SyncManager* sync_manager) { REALM_ASSERT(config.sync_config); - return std::make_shared(Private(), client, std::move(db), config, std::move(sync_manager)); + return std::make_shared(Private(), client, std::move(db), config, sync_manager); } // } - std::shared_ptr sync_manager() const REQUIRES(!m_state_mutex); + SyncManager* sync_manager() const REQUIRES(!m_state_mutex); static util::UniqueFunction)> handle_refresh(const std::shared_ptr&, bool); diff --git a/src/realm/object-store/sync/sync_user.cpp b/src/realm/object-store/sync/sync_user.cpp deleted file mode 100644 index edac5583a6d..00000000000 --- a/src/realm/object-store/sync/sync_user.cpp +++ /dev/null @@ -1,424 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace realm { - -SyncUserIdentity::SyncUserIdentity(const std::string& id, const std::string& provider_type) - : id(id) - , provider_type(provider_type) -{ -} - -SyncUser::SyncUser(Private, const std::string& refresh_token, const std::string& id, const std::string& access_token, - const std::string& device_id, SyncManager* sync_manager) - : m_state(State::LoggedIn) - , m_identity(id) - , m_refresh_token(RealmJWT(refresh_token)) - , m_access_token(RealmJWT(access_token)) - , m_device_id(device_id) - , m_sync_manager(sync_manager) -{ - REALM_ASSERT(!access_token.empty() && !refresh_token.empty()); - - m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedIn, m_access_token.token, m_refresh_token.token); - metadata->set_device_id(m_device_id); - m_legacy_identities = metadata->legacy_identities(); - this->m_user_profile = metadata->profile(); - }); -} - -SyncUser::SyncUser(Private, const SyncUserMetadata& data, SyncManager* sync_manager) - : m_state(data.state()) - , m_legacy_identities(data.legacy_identities()) - , m_identity(data.identity()) - , m_refresh_token(RealmJWT(data.refresh_token())) - , m_access_token(RealmJWT(data.access_token())) - , m_user_identities(data.identities()) - , m_user_profile(data.profile()) - , m_device_id(data.device_id()) - , m_sync_manager(sync_manager) -{ - REALM_ASSERT(m_state == State::LoggedIn && !m_access_token.token.empty() && !m_refresh_token.token.empty()); -} - -std::shared_ptr SyncUser::sync_manager() const -{ - util::CheckedLockGuard lk(m_mutex); - if (m_state == State::Removed) { - throw app::AppError( - ErrorCodes::ClientUserNotFound, - util::format("Cannot start a sync session for user '%1' because this user has been removed.", - m_identity)); - } - REALM_ASSERT(m_sync_manager); - return m_sync_manager->shared_from_this(); -} - -void SyncUser::detach_from_sync_manager() -{ - util::CheckedLockGuard lk(m_mutex); - REALM_ASSERT(m_sync_manager); - m_state = SyncUser::State::Removed; - m_sync_manager = nullptr; -} - -std::vector> SyncUser::all_sessions() -{ - util::CheckedLockGuard lock(m_mutex); - std::vector> sessions; - if (m_state == State::Removed) { - return sessions; - } - for (auto it = m_sessions.begin(); it != m_sessions.end();) { - if (auto ptr_to_session = it->second.lock()) { - sessions.emplace_back(std::move(ptr_to_session)); - it++; - continue; - } - // This session is bad, destroy it. - it = m_sessions.erase(it); - } - return sessions; -} - -std::shared_ptr SyncUser::session_for_on_disk_path(const std::string& path) -{ - util::CheckedLockGuard lock(m_mutex); - if (m_state == State::Removed) { - return nullptr; - } - auto it = m_sessions.find(path); - if (it == m_sessions.end()) { - return nullptr; - } - auto locked = it->second.lock(); - if (!locked) { - // Remove the session from the map, because it has fatally errored out or the entry is invalid. - m_sessions.erase(it); - } - return locked; -} - -void SyncUser::log_in(const std::string& access_token, const std::string& refresh_token) -{ - REALM_ASSERT(!access_token.empty()); - REALM_ASSERT(!refresh_token.empty()); - std::vector> sessions_to_revive; - { - util::CheckedLockGuard lock1(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - m_state = State::LoggedIn; - m_access_token = RealmJWT(access_token); - m_refresh_token = RealmJWT(refresh_token); - sessions_to_revive = revive_sessions(); - - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedIn, access_token, refresh_token); - }); - } - // (Re)activate all pending sessions. - // Note that we do this after releasing the lock, since the session may - // need to access protected User state in the process of binding itself. - for (auto& session : sessions_to_revive) { - session->revive_if_needed(); - } - - emit_change_to_subscribers(*this); -} - -void SyncUser::invalidate() -{ - { - util::CheckedLockGuard lock1(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - m_state = State::Removed; - m_access_token = {}; - m_refresh_token = {}; - - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::Removed, "", ""); - }); - } - emit_change_to_subscribers(*this); -} - -std::vector> SyncUser::revive_sessions() -{ - std::vector> sessions_to_revive; - sessions_to_revive.reserve(m_waiting_sessions.size()); - for (auto& [path, weak_session] : m_waiting_sessions) { - if (auto ptr = weak_session.lock()) { - m_sessions[path] = ptr; - sessions_to_revive.emplace_back(std::move(ptr)); - } - } - m_waiting_sessions.clear(); - return sessions_to_revive; -} - -void SyncUser::update_access_token(std::string&& token) -{ - { - util::CheckedLockGuard lock(m_mutex); - if (m_state != State::LoggedIn) - return; - - util::CheckedLockGuard lock2(m_tokens_mutex); - m_access_token = RealmJWT(std::move(token)); - m_sync_manager->perform_metadata_update([&, raw_access_token = m_access_token.token](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_access_token(raw_access_token); - }); - } - - emit_change_to_subscribers(*this); -} - -std::vector SyncUser::identities() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_user_identities; -} - -void SyncUser::log_out() -{ - // We'll extend the lifetime of SyncManager while holding m_mutex so that we know it's safe to call methods on it - // after we've been marked as logged out. - std::shared_ptr sync_manager_shared; - { - util::CheckedLockGuard lock(m_mutex); - bool is_anonymous = false; - { - util::CheckedLockGuard lock2(m_tokens_mutex); - if (m_state != State::LoggedIn) { - return; - } - is_anonymous = do_is_anonymous(); - m_state = State::LoggedOut; - m_access_token = RealmJWT{}; - m_refresh_token = RealmJWT{}; - } - - if (is_anonymous) { - // An Anonymous user can not log back in. - // Mark the user as 'dead' in the persisted metadata Realm. - m_state = State::Removed; - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity, false); - if (metadata) - metadata->remove(); - }); - } - else { - m_sync_manager->perform_metadata_update([&](const auto& manager) { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_state_and_tokens(State::LoggedOut, "", ""); - }); - } - sync_manager_shared = m_sync_manager->shared_from_this(); - // Move all active sessions into the waiting sessions pool. If the user is - // logged back in, they will automatically be reactivated. - for (auto& [path, weak_session] : m_sessions) { - if (auto ptr = weak_session.lock()) { - ptr->force_close(); - m_waiting_sessions[path] = std::move(ptr); - } - } - m_sessions.clear(); - } - - sync_manager_shared->log_out_user(*this); - emit_change_to_subscribers(*this); -} - -bool SyncUser::is_logged_in() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_state == State::LoggedIn; -} - -bool SyncUser::is_anonymous() const -{ - util::CheckedLockGuard lock(m_mutex); - util::CheckedLockGuard lock2(m_tokens_mutex); - return do_is_anonymous(); -} - -bool SyncUser::do_is_anonymous() const -{ - return m_state == State::LoggedIn && m_user_identities.size() == 1 && - m_user_identities[0].provider_type == app::IdentityProviderAnonymous; -} - -std::string SyncUser::refresh_token() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_refresh_token.token; -} - -std::string SyncUser::access_token() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_access_token.token; -} - -std::string SyncUser::device_id() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_device_id; -} - -bool SyncUser::has_device_id() const -{ - util::CheckedLockGuard lock(m_mutex); - return !m_device_id.empty() && m_device_id != "000000000000000000000000"; -} - -SyncUser::State SyncUser::state() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_state; -} - -SyncUserProfile SyncUser::user_profile() const -{ - util::CheckedLockGuard lock(m_mutex); - return m_user_profile; -} - -util::Optional SyncUser::custom_data() const -{ - util::CheckedLockGuard lock(m_tokens_mutex); - return m_access_token.user_data; -} - -void SyncUser::update_user_profile(std::vector identities, SyncUserProfile profile) -{ - util::CheckedLockGuard lock(m_mutex); - if (m_state == SyncUser::State::Removed) { - return; - } - - m_user_identities = std::move(identities); - m_user_profile = std::move(profile); - - m_sync_manager->perform_metadata_update([&](const auto& manager) NO_THREAD_SAFETY_ANALYSIS { - auto metadata = manager.get_or_make_user_metadata(m_identity); - metadata->set_identities(m_user_identities); - metadata->set_user_profile(m_user_profile); - }); -} - -void SyncUser::register_session(std::shared_ptr session) -{ - const std::string& path = session->path(); - util::CheckedUniqueLock lock(m_mutex); - switch (m_state) { - case State::LoggedIn: - m_sessions[path] = session; - break; - case State::LoggedOut: - m_waiting_sessions[path] = session; - break; - case State::Removed: - break; - } -} - -app::MongoClient SyncUser::mongo_client(const std::string& service_name) -{ - util::CheckedLockGuard lk(m_mutex); - REALM_ASSERT(m_state == SyncUser::State::LoggedIn); - return app::MongoClient(shared_from_this(), m_sync_manager->app().lock(), service_name); -} - -void SyncUser::refresh_custom_data(util::UniqueFunction)> completion_block) - REQUIRES(!m_mutex) -{ - refresh_custom_data(false, std::move(completion_block)); -} - -void SyncUser::refresh_custom_data(bool update_location, - util::UniqueFunction)> completion_block) -{ - std::shared_ptr app; - std::shared_ptr user; - { - util::CheckedLockGuard lk(m_mutex); - if (m_state != SyncUser::State::Removed) { - user = shared_from_this(); - } - if (m_sync_manager) { - app = m_sync_manager->app().lock(); - } - } - if (!user) { - completion_block(app::AppError( - ErrorCodes::ClientUserNotFound, - util::format("Cannot initiate a refresh on user '%1' because the user has been removed", m_identity))); - } - else if (!app) { - completion_block(app::AppError( - ErrorCodes::ClientAppDeallocated, - util::format("Cannot initiate a refresh on user '%1' because the app has been deallocated", m_identity))); - } - else { - std::weak_ptr weak_user = user->weak_from_this(); - app->refresh_custom_data(user, update_location, - [completion_block = std::move(completion_block), weak_user](auto error) { - if (auto strong = weak_user.lock()) { - strong->emit_change_to_subscribers(*strong); - } - completion_block(error); - }); - } -} - -bool SyncUser::access_token_refresh_required() const -{ - using namespace std::chrono; - constexpr size_t buffer_seconds = 5; // arbitrary - util::CheckedLockGuard lock(m_tokens_mutex); - const auto now = duration_cast(system_clock::now().time_since_epoch()).count() + - m_seconds_to_adjust_time_for_testing.load(std::memory_order_relaxed); - const auto threshold = now - buffer_seconds; - return !m_access_token.token.empty() && m_access_token.expires_at < static_cast(threshold); -} - -} // namespace realm - -namespace std { -size_t hash::operator()(const realm::SyncUserIdentity& k) const -{ - return ((hash()(k.id) ^ (hash()(k.provider_type) << 1)) >> 1); -} -} // namespace std diff --git a/src/realm/object-store/sync/sync_user.hpp b/src/realm/object-store/sync/sync_user.hpp index 00e39c15d1d..28ef2ec9bbd 100644 --- a/src/realm/object-store/sync/sync_user.hpp +++ b/src/realm/object-store/sync/sync_user.hpp @@ -20,282 +20,83 @@ #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 - { - auto it = m_data.find(name); - if (it == m_data.end()) { - return util::none; - } - return static_cast((*it).second); - } +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 4009713b6c2..50239651976 100644 --- a/src/realm/sync/client.cpp +++ b/src/realm/sync/client.cpp @@ -110,7 +110,7 @@ class SessionWrapper final : public util::AtomicRefCountBase, DB::CommitListener bool wait_for_upload_complete_or_client_stopped(); bool wait_for_download_complete_or_client_stopped(); - void refresh(std::string signed_access_token); + void refresh(std::string_view signed_access_token); static void abandon(util::bind_ptr) noexcept; @@ -1501,13 +1501,13 @@ bool SessionWrapper::wait_for_download_complete_or_client_stopped() } -void SessionWrapper::refresh(std::string signed_access_token) +void SessionWrapper::refresh(std::string_view signed_access_token) { // Thread safety required REALM_ASSERT(m_initiated); REALM_ASSERT(!m_abandoned); - m_client.post([self = util::bind_ptr(this), token = std::move(signed_access_token)](Status status) { + m_client.post([self = util::bind_ptr(this), token = std::string(signed_access_token)](Status status) { if (status == ErrorCodes::OperationAborted) return; else if (!status.is_ok()) @@ -2134,7 +2134,7 @@ bool Session::wait_for_download_complete_or_client_stopped() } -void Session::refresh(const std::string& signed_access_token) +void Session::refresh(std::string_view signed_access_token) { m_impl->refresh(signed_access_token); // Throws } diff --git a/src/realm/sync/client.hpp b/src/realm/sync/client.hpp index f38f35f3e6b..1e05f6186b1 100644 --- a/src/realm/sync/client.hpp +++ b/src/realm/sync/client.hpp @@ -564,7 +564,7 @@ class Session { /// /// \param signed_user_token A cryptographically signed token describing the /// identity and access rights of the current user. See ProtocolEnvelope. - void refresh(const std::string& signed_user_token); + void refresh(std::string_view signed_user_token); /// \brief Inform the synchronization agent about changes of local origin. /// diff --git a/src/realm/sync/socket_provider.hpp b/src/realm/sync/socket_provider.hpp index c9348a7df81..ee51754c7b4 100644 --- a/src/realm/sync/socket_provider.hpp +++ b/src/realm/sync/socket_provider.hpp @@ -18,19 +18,17 @@ #pragma once -#include -#include -#include - #include - #include #include - #include #include #include +#include +#include +#include + namespace realm::sync { namespace websocket { enum class WebSocketError; diff --git a/src/realm/util/file.cpp b/src/realm/util/file.cpp index 5e9e52ebe11..84bbe407c93 100644 --- a/src/realm/util/file.cpp +++ b/src/realm/util/file.cpp @@ -395,19 +395,16 @@ std::string make_temp_file(const char* prefix) char* tmp_dir_env = getenv("TMPDIR"); std::string base_dir = tmp_dir_env ? tmp_dir_env : std::string(P_tmpdir); if (!base_dir.empty() && base_dir[base_dir.length() - 1] != '/') { - base_dir = base_dir + "/"; + base_dir += '/'; } #endif - std::string tmp = base_dir + prefix + std::string("_XXXXXX") + std::string("\0", 1); - std::unique_ptr buffer = std::make_unique(tmp.size()); // Throws - memcpy(buffer.get(), tmp.c_str(), tmp.size()); - char* filename = buffer.get(); - auto fd = mkstemp(filename); + std::string filename = util::format("%1%2_XXXXXX", base_dir, prefix); + auto fd = mkstemp(filename.data()); if (fd == -1) { throw std::system_error(errno, std::system_category(), "mkstemp() failed"); // LCOV_EXCL_LINE } close(fd); - return std::string(filename); + return filename; #endif } diff --git a/src/realm/util/random.hpp b/src/realm/util/random.hpp index 541252ede84..fc826efe162 100644 --- a/src/realm/util/random.hpp +++ b/src/realm/util/random.hpp @@ -22,8 +22,6 @@ namespace util { /// up the engine state. /// /// Thread-safe. -/// -/// FIXME: Move this to core repo, as it is generally useful. template void seed_prng_nondeterministically(Engine&); diff --git a/test/object-store/CMakeLists.txt b/test/object-store/CMakeLists.txt index aac511f7428..0a795cd3db9 100644 --- a/test/object-store/CMakeLists.txt +++ b/test/object-store/CMakeLists.txt @@ -70,7 +70,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 a7b6547fa5e..7ba95f2965b 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"}; @@ -300,6 +300,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; @@ -1073,6 +1074,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); @@ -1502,6 +1504,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(); @@ -1536,7 +1539,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; @@ -1607,6 +1610,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); @@ -1633,6 +1637,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); @@ -1692,6 +1697,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; @@ -1750,6 +1756,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); @@ -1787,7 +1794,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"); @@ -1809,6 +1816,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 a2bf75571b8..67dbbb515b4 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 9464efded94..72e14b66ee8 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -592,11 +592,10 @@ TEST_CASE("C API (non-database)", "[c_api]") { CHECK(app_config->device_info.bundle_id == "some_bundle_id"); test_util::TestDirGuard temp_dir(util::make_temp_dir()); - auto sync_client_config = cptr(realm_sync_client_config_new()); - realm_sync_client_config_set_base_file_path(sync_client_config.get(), temp_dir.c_str()); - realm_sync_client_config_set_metadata_mode(sync_client_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); + realm_app_config_set_base_file_path(app_config.get(), temp_dir.c_str()); + realm_app_config_set_metadata_mode(app_config.get(), RLM_SYNC_CLIENT_METADATA_MODE_DISABLED); - auto test_app = cptr(realm_app_create(app_config.get(), sync_client_config.get())); + auto test_app = cptr(realm_app_create(app_config.get())); realm_user_t* sync_user; auto user_data_free = [](realm_userdata_t) {}; @@ -5261,7 +5260,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); @@ -5293,32 +5292,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; @@ -5334,8 +5329,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); @@ -5842,8 +5836,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); @@ -5858,7 +5851,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 a347839a3e4..fddb1b84213 100644 --- a/test/object-store/realm.cpp +++ b/test/object-store/realm.cpp @@ -47,8 +47,9 @@ #include #include -#include +#include #include + #include #include #endif @@ -1137,11 +1138,24 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { auto expired_token = encode_fake_jwt("", 123, 456); SECTION("can async open while waiting for a token refresh") { - SyncTestFile config(tsm, "realm"); - auto user = config.sync_config->user; + struct User : TestUser { + using TestUser::TestUser; + CompletionHandler stored_completion; + void request_access_token(CompletionHandler&& completion) override + { + stored_completion = std::move(completion); + } + bool access_token_refresh_required() const override + { + return !stored_completion; + } + }; + auto user = std::make_shared("realm", tsm.sync_manager()); + SyncTestFile config(user, "realm"); auto valid_token = user->access_token(); - user->update_access_token(std::move(expired_token)); + user->m_access_token = expired_token; + REQUIRE_FALSE(user->stored_completion); std::atomic called{false}; auto task = Realm::get_synchronized_realm(config); task->start([&](auto ref, auto error) { @@ -1150,11 +1164,10 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { REQUIRE(!error); called = true; }); - auto session = tsm.sync_manager()->get_existing_session(config.path); - REQUIRE(session); - CHECK(session->state() == SyncSession::State::WaitingForAccessToken); + REQUIRE(user->stored_completion); + user->m_access_token = valid_token; + user->stored_completion({}); - session->update_access_token(valid_token); util::EventLoop::main().run_until([&] { return called.load(); }); @@ -1163,25 +1176,21 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { } SECTION("cancels download and reports an error on auth error") { - struct Transport : UnitTestTransport { - void send_request_to_server( - const realm::app::Request& req, - realm::util::UniqueFunction&& completion) override + struct User : TestUser { + using TestUser::TestUser; + void request_access_token(CompletionHandler&& completion) override { - if (req.url.find("/auth/session") != std::string::npos) { - completion(app::Response{403}); - } - else { - UnitTestTransport::send_request_to_server(req, std::move(completion)); - } + completion(app::AppError(ErrorCodes::HTTPError, "403 error", "", 403)); + } + bool access_token_refresh_required() const override + { + return true; } }; - OfflineAppSession::Config oas_config; - oas_config.transport = std::make_shared(); - OfflineAppSession oas(oas_config); - - SyncTestFile config(oas, "realm"); - config.sync_config->user->log_in(expired_token, expired_token); + auto user = std::make_shared("realm", tsm.sync_manager()); + user->m_access_token = expired_token; + user->m_refresh_token = expired_token; + SyncTestFile config(user, "realm"); bool got_error = false; config.sync_config->error_handler = [&](std::shared_ptr, SyncError) { @@ -1192,9 +1201,8 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { task->start([&](auto ref, auto error) { std::lock_guard lock(mutex); REQUIRE(error); - REQUIRE_EXCEPTION( - std::rethrow_exception(error), HTTPError, - "Unable to refresh the user access token: http error code considered fatal. Client Error: 403"); + REQUIRE_EXCEPTION(std::rethrow_exception(error), HTTPError, + "Unable to refresh the user access token: 403 error. Client Error: 403"); REQUIRE(!ref); called = true; }); diff --git a/test/object-store/sync/app.cpp b/test/object-store/sync/app.cpp index 77e25b950f0..23f61f7c9ce 100644 --- a/test/object-store/sync/app.cpp +++ b/test/object-store/sync/app.cpp @@ -21,6 +21,7 @@ #include "util/sync/sync_test_utils.hpp" #include "util/test_path.hpp" #include "util/unit_test_transport.hpp" +#include "util/test_path.hpp" #include #include @@ -64,13 +65,13 @@ using namespace Catch::Matchers; namespace { -std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) +std::shared_ptr log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) { if (auto transport = dynamic_cast(app->config().transport.get())) { transport->set_provider_type(credentials.provider_as_string()); } - std::shared_ptr user; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user_arg, Optional error) { + std::shared_ptr user; + app->log_in_with_credentials(credentials, [&](std::shared_ptr user_arg, Optional error) { REQUIRE_FALSE(error); REQUIRE(user_arg); user = std::move(user_arg); @@ -82,7 +83,7 @@ std::shared_ptr log_in(std::shared_ptr app, AppCredentials creden AppError failed_log_in(std::shared_ptr app, AppCredentials credentials = AppCredentials::anonymous()) { Optional err; - app->log_in_with_credentials(credentials, [&](std::shared_ptr user, Optional error) { + app->log_in_with_credentials(credentials, [&](std::shared_ptr user, Optional error) { REQUIRE(error); REQUIRE_FALSE(user); err = error; @@ -785,7 +786,7 @@ TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][ SECTION("cannot login with wrong password") { app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"), - [&](std::shared_ptr user, Optional error) { + [&](std::shared_ptr user, Optional error) { CHECK(!user); REQUIRE(error); REQUIRE(error->code() == ErrorCodes::InvalidPassword); @@ -922,7 +923,7 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa App::UserAPIKey api_key; SECTION("api-key") { - std::shared_ptr logged_in_user = app->current_user(); + std::shared_ptr logged_in_user = app->current_user(); auto api_key_name = util::format("%1", random_string(15)); client.create_api_key(api_key_name, logged_in_user, [&](App::UserAPIKey user_api_key, Optional error) { @@ -982,7 +983,7 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa } SECTION("api-key without a user") { - std::shared_ptr no_user = nullptr; + std::shared_ptr no_user = nullptr; auto api_key_name = util::format("%1", random_string(15)); client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional error) { REQUIRE(error); @@ -1048,9 +1049,9 @@ TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baa } SECTION("api-key against the wrong user") { - std::shared_ptr first_user = app->current_user(); + std::shared_ptr first_user = app->current_user(); create_user_and_log_in(app); - std::shared_ptr second_user = app->current_user(); + std::shared_ptr second_user = app->current_user(); REQUIRE(first_user != second_user); auto api_key_name = util::format("%1", random_string(15)); App::UserAPIKey api_key; @@ -1208,7 +1209,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { REQUIRE(user->identities().size() == 1); CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous); - app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { + app->link_user(user, creds, [&](std::shared_ptr user2, Optional error) { REQUIRE_FALSE(error); REQUIRE(user == user2); REQUIRE(user->identities().size() == 2); @@ -1218,7 +1219,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { } SECTION("linking an identity makes the user no longer returned by anonymous logins") { - app->link_user(user, creds, [&](std::shared_ptr, Optional error) { + app->link_user(user, creds, [&](std::shared_ptr, Optional error) { REQUIRE_FALSE(error); }); auto user2 = log_in(app); @@ -1226,7 +1227,7 @@ TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") { } SECTION("existing users are reused when logging in via linked identities") { - app->link_user(user, creds, [](std::shared_ptr, Optional error) { + app->link_user(user, creds, [](std::shared_ptr, Optional error) { REQUIRE_FALSE(error); }); app->log_out([](auto error) { @@ -1305,7 +1306,7 @@ TEST_CASE("app: delete user with credentials integration", "[sync][app][user][ba CHECK(user->state() == SyncUser::State::Removed); CHECK(app->current_user() == nullptr); - app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { + app->log_in_with_credentials(credentials, [](std::shared_ptr user, util::Optional error) { CHECK(!user); REQUIRE(error); REQUIRE(error->code() == ErrorCodes::InvalidPassword); @@ -2013,7 +2014,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; @@ -2095,8 +2096,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(); @@ -2175,7 +2178,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)); @@ -2218,7 +2221,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); @@ -2235,7 +2238,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)); @@ -2384,7 +2387,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); @@ -2444,7 +2447,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)); @@ -2487,9 +2490,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 { @@ -2628,7 +2631,7 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { SECTION("Add Objects") { { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(get_dogs(r).size() == 0); @@ -2638,7 +2641,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); @@ -2649,7 +2652,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(); @@ -2663,7 +2666,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); @@ -2685,21 +2688,22 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } } SynchronousTestTransport::send_request_to_server(request, [&](const Response& response) mutable { + Response modifiable_response = response; if (response_hook) { - response_hook(request, response); + response_hook(request, modifiable_response); } - completion(response); + completion(modifiable_response); }); } // Optional handler for the request and response before it is returned to completion - std::function response_hook; + std::function response_hook; // Optional handler for the request before it is sent to the server std::function(const Request&)> request_hook; }; SECTION("Fast clock on client") { { - SyncTestFile config(app, partition, schema); + SyncTestFile config(app->current_user(), partition, schema); auto r = Realm::get_shared_realm(config); REQUIRE(get_dogs(r).size() == 0); @@ -2710,10 +2714,10 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto transport = std::make_shared(); TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); auto app = hooked_session.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); - // Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally + // Make the User behave as if the client clock is 31 minutes fast, so the token looks expired locally // (access tokens have an lifetime of 30 minutes today). user->set_seconds_to_adjust_time_for_testing(31 * 60); REQUIRE(user->access_token_refresh_required()); @@ -2724,7 +2728,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) { @@ -2734,7 +2738,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); @@ -2746,8 +2750,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); @@ -2768,11 +2772,13 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { auto transport = std::make_shared(); TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false}); auto app = hooked_session.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. - user->update_access_token(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); + user->update_data_for_testing([&token](UserData& data) { + data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", token.expires, token.timestamp)); + }); REQUIRE(user->access_token_refresh_required()); SECTION("Expired Access Token is Refreshed") { @@ -2782,7 +2788,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; @@ -2790,7 +2796,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); @@ -2801,17 +2807,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); @@ -2833,8 +2848,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); } @@ -2845,15 +2870,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 { @@ -2862,7 +2886,12 @@ TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") { } return std::nullopt; }; - SyncTestFile config(app, partition, schema); + SyncTestFile config(user, partition, schema); + config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) { + REQUIRE(error.status.code() == ErrorCodes::AuthError); + REQUIRE_THAT(std::string{error.status.reason()}, + Catch::Matchers::StartsWith("Unable to refresh the user access token")); + }; auto r = Realm::get_shared_realm(config); create_one_dog(r); timed_wait_for( @@ -2904,8 +2933,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)); @@ -2921,7 +2949,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(); @@ -2934,7 +2964,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()); { @@ -2964,11 +2994,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); @@ -2977,7 +3007,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); @@ -2995,10 +3025,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)); @@ -3029,9 +3059,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)); @@ -3048,28 +3078,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); @@ -3079,14 +3111,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; @@ -3097,7 +3129,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; @@ -3135,7 +3167,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 = @@ -3174,7 +3206,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); @@ -3201,7 +3233,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; { @@ -3242,7 +3274,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"; @@ -3254,7 +3286,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(); }); @@ -3288,7 +3320,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); @@ -3755,30 +3787,31 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } }; - auto redir_transport = std::make_shared(); util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); auto logger = util::Logger::get_default_logger(); - App::Config app_config = {"app_id"}; - set_app_config_defaults(app_config, redir_transport); - - SyncClientConfig sc_config; - sc_config.base_file_path = util::make_temp_dir(); - sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; + auto redir_transport = std::make_shared(); + auto get_config_with_base_url = [&](std::optional base_url = std::nullopt) { + OfflineAppSession::Config config(redir_transport); + config.base_url = base_url; + return config; + }; SECTION("Test app config baseurl") { { + // First time through, base_url is empty; https://realm.mongodb.com is expected redir_transport->reset("https://realm.mongodb.com"); + auto config = get_config_with_base_url(); + OfflineAppSession oas(config); + auto app = oas.app(); - // First time through, base_url is empty; https://realm.mongodb.com is expected - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://realm.mongodb.com"); CHECK(app->get_ws_host_url() == "wss://realm.mongodb.com"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://realm.mongodb.com"); CHECK(app->get_host_url() == "https://realm.mongodb.com"); @@ -3786,17 +3819,18 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } { // Second time through, base_url is set to https://alternate.someurl.fake is expected - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://alternate.someurl.fake"); CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3805,19 +3839,20 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { { // Third time through, base_url is not set, expect https://realm.mongodb.com, since metadata // is no longer used - app_config.base_url = util::none; std::string expected_url = "https://realm.mongodb.com"; std::string expected_wsurl = "wss://realm.mongodb.com"; redir_transport->reset(expected_url); + auto config = get_config_with_base_url(); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == expected_url); CHECK(app->get_ws_host_url() == expected_wsurl); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == expected_url); CHECK(app->get_host_url() == expected_url); @@ -3825,17 +3860,18 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } { // Fourth time through, base_url is set to https://some-other.someurl.fake, with a redirect - app_config.base_url = "https://some-other.someurl.fake"; redir_transport->reset("https://some-other.someurl.fake", "http://redirect.someurl.fake"); + auto config = get_config_with_base_url("https://some-other.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); // Initial hostname and ws hostname use base url, but aren't used until location is updated CHECK(app->get_host_url() == "https://some-other.someurl.fake"); CHECK(app->get_ws_host_url() == "wss://some-other.someurl.fake"); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); // Base URL is still set to the original value CHECK(app->get_base_url() == "https://some-other.someurl.fake"); @@ -3846,14 +3882,15 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { } SECTION("Test update_baseurl") { - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3870,18 +3907,19 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK(app->get_host_url() == "https://realm.mongodb.com"); CHECK(app->get_ws_host_url() == "wss://realm.mongodb.com"); // Expected URL is still "https://realm.mongodb.com" - create_user_and_log_in(app); + oas.make_user(); } SECTION("Test update_baseurl with redirect") { - app_config.base_url = "https://alternate.someurl.fake"; redir_transport->reset("https://alternate.someurl.fake"); + auto config = get_config_with_base_url("https://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "https://alternate.someurl.fake"); CHECK(app->get_host_url() == "https://alternate.someurl.fake"); @@ -3897,18 +3935,19 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK(app->get_host_url() == "https://redirect.otherurl.fake"); CHECK(app->get_ws_host_url() == "wss://redirect.otherurl.fake"); // Expected URL is still "https://redirect.otherurl.fake" after redirect - create_user_and_log_in(app); + oas.make_user(); } SECTION("Test update_baseurl returns error") { - app_config.base_url = "http://alternate.someurl.fake"; redir_transport->reset("http://alternate.someurl.fake"); + auto config = get_config_with_base_url("http://alternate.someurl.fake"); + OfflineAppSession oas(config); + auto app = oas.app(); - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); // Location is not requested until first app services request CHECK(!redir_transport->location_requested); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == "http://alternate.someurl.fake"); CHECK(app->get_host_url() == "http://alternate.someurl.fake"); @@ -3938,8 +3977,7 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { std::string redir_wsurl = util::format("ws%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port); auto socket_provider = std::make_shared(logger, "some user agent"); - socket_provider->endpoint_verify_func = [&use_ssl, &expected_host, - &expected_port](sync::WebSocketEndpoint& ep) { + socket_provider->endpoint_verify_func = [use_ssl, expected_host, expected_port](sync::WebSocketEndpoint& ep) { CHECK(ep.address == expected_host); CHECK(ep.port == expected_port); CHECK(ep.is_ssl == use_ssl); @@ -3952,20 +3990,21 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { return true; }; - sc_config.socket_provider = socket_provider; - sc_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - - app_config.base_url = init_url; + auto config = get_config_with_base_url(init_url); + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.socket_provider = socket_provider; + config.storage_path = util::make_temp_dir(); + config.delete_storage = false; // persist the current user // Log in to get a cached user { redir_transport->reset(init_url); - - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); - create_user_and_log_in(app); + oas.make_user(); CHECK(redir_transport->location_requested); CHECK(app->get_base_url() == init_url); CHECK(app->get_host_url() == init_url); @@ -3974,19 +4013,21 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { CHECK_THAT(*app->sync_manager()->sync_route(), ContainsSubstring(init_wsurl)); } + // the next instance can clean up the files + config.delete_storage = true; // Recreate the app using the cached user and start a sync session, which will is set to fail on connect SECTION("Sync Session fails on connect") { enum class TestState { start, session_started }; TestingStateMachine state(TestState::start); - redir_transport->reset(init_url, redir_url); - - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); + REQUIRE(app->current_user()); RealmConfig r_config; - r_config.path = sc_config.base_file_path + "/fakerealm.realm"; + r_config.path = app->config().base_file_path + "/fakerealm.realm"; r_config.sync_config = std::make_shared(app->current_user(), SyncConfig::FLXSyncEnabled{}); r_config.sync_config->error_handler = [&state, &logger](std::shared_ptr, SyncError error) mutable { @@ -3994,7 +4035,7 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { state.transition_with([&error, &logger](TestState cur_state) -> std::optional { if (cur_state == TestState::start) { // The session will start, but the connection is rejected on purpose - logger->debug("Expected error: %1", error.status); + logger->debug("Expected error: %1: %2", error.status.code_string(), error.status.reason()); CHECK(!error.status.is_ok()); CHECK(error.status.code() == ErrorCodes::SyncConnectFailed); return TestState::session_started; @@ -4016,16 +4057,17 @@ TEST_CASE("app: base_url", "[sync][app][base_url]") { SECTION("Location update fails prior to sync session connect") { enum class TestState { start, location_failed, waiting_for_session, session_started }; TestingStateMachine state(TestState::start); - redir_transport->reset(init_url, redir_url); redir_transport->location_returns_error = true; - auto app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + OfflineAppSession oas(config); + auto app = oas.app(); // At this point, the sync route is not set CHECK(!app->sync_manager()->sync_route()); + REQUIRE(app->current_user()); RealmConfig r_config; - r_config.path = sc_config.base_file_path + "/fakerealm.realm"; + r_config.path = app->config().base_file_path + "/fakerealm.realm"; r_config.sync_config = std::make_shared(app->current_user(), SyncConfig::FLXSyncEnabled{}); r_config.sync_config->error_handler = [&state, &logger](std::shared_ptr, SyncError error) mutable { @@ -4109,7 +4151,7 @@ TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][func SECTION("jwt happy path") { bool processed = false; - std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); + std::shared_ptr user = log_in(app, AppCredentials::custom(jwt)); app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})}, [&](auto response, auto error) { @@ -4203,12 +4245,12 @@ 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 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 auto r2 = realm::Realm::get_shared_realm(config2); Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source")); @@ -4383,7 +4425,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"); @@ -4449,10 +4491,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!"); @@ -4538,19 +4578,18 @@ TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") { static_cast(config.transport.get())->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; + UnitTestTransport::access_token = good_access_token; { + 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); @@ -4562,7 +4601,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 @@ -4572,7 +4610,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); @@ -4603,7 +4641,7 @@ 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); @@ -4664,9 +4702,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 = [=] { @@ -4685,28 +4722,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 @@ -4720,8 +4757,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); @@ -4731,14 +4768,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); @@ -4762,7 +4799,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); }); @@ -4780,14 +4817,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); @@ -5048,7 +5085,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); @@ -5390,7 +5427,9 @@ TEST_CASE("app: app destroyed during token refresh", "[app][user][token]") { OfflineAppSession oas({transport}); SyncTestFile config(oas); // give the user an expired access token so that the first use will try to refresh it - config.sync_config->user->update_access_token(encode_fake_jwt("token", 123, 456)); + oas.app()->current_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->stored_completion); @@ -5427,7 +5466,7 @@ TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") { return out; }; - const auto make_request = [&](std::shared_ptr user, auto&&... args) { + const auto make_request = [&](std::shared_ptr user, auto&&... args) { auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"}); CHECK(req.method == HttpMethod::get); CHECK(req.body == ""); @@ -5473,7 +5512,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); @@ -5485,7 +5524,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"}, @@ -5547,17 +5586,17 @@ TEST_CASE("app: app cannot get deallocated during log in", "[sync][app]") { TestingStateMachine& state; }; - auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); + auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); auto transporter = std::make_shared(mock_transport_worker, state); { OfflineAppSession oas({transporter}); - oas.app()->log_in_with_credentials( - AppCredentials::anonymous(), [promise = std::move(cur_user_promise)]( - std::shared_ptr user, util::Optional error) mutable { - REQUIRE_FALSE(error); - promise.emplace_value(std::move(user)); - }); + oas.app()->log_in_with_credentials(AppCredentials::anonymous(), + [promise = std::move(cur_user_promise)]( + std::shared_ptr user, util::Optional error) mutable { + REQUIRE_FALSE(error); + promise.emplace_value(std::move(user)); + }); } // At this point the test does not hold any reference to `app`, but the @@ -5632,14 +5671,14 @@ TEST_CASE("app: user logs out while profile is fetched", "[sync][app][user]") { transporter->set_app(app.get()); auto custom_credentials = AppCredentials::facebook("a_token"); - auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); + auto [cur_user_promise, cur_user_future] = util::make_promise_future>(); - app->log_in_with_credentials(custom_credentials, - [promise = std::move(cur_user_promise)](std::shared_ptr user, - util::Optional error) mutable { - REQUIRE_FALSE(error); - promise.emplace_value(std::move(user)); - }); + app->log_in_with_credentials( + custom_credentials, + [promise = std::move(cur_user_promise)](std::shared_ptr user, util::Optional error) mutable { + REQUIRE_FALSE(error); + promise.emplace_value(std::move(user)); + }); auto cur_user = std::move(cur_user_future).get(); CHECK(state.get() == TestState::profile); @@ -5652,12 +5691,10 @@ TEST_CASE("app: user logs out while profile is fetched", "[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.metadata_mode = AppConfig::MetadataMode::InMemory; + base_config.base_file_path = test_dir; auto config1 = base_config; config1.app_id = "app1"; @@ -5674,10 +5711,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); @@ -5686,9 +5723,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 f45a3a95707..7c2724daff3 100644 --- a/test/object-store/sync/client_reset.cpp +++ b/test/object-store/sync/client_reset.cpp @@ -326,7 +326,7 @@ TEST_CASE("sync: client reset", "[sync][pbs][client reset][baas]") { recovery_path = recovery_path_it->second; REQUIRE(util::File::exists(orig_path)); REQUIRE(!util::File::exists(recovery_path)); - bool did_reset_files = test_app_session.sync_manager()->immediately_run_file_actions(orig_path); + bool did_reset_files = test_app_session.app()->immediately_run_file_actions(orig_path); REQUIRE(did_reset_files); REQUIRE(!util::File::exists(orig_path)); REQUIRE(util::File::exists(recovery_path)); diff --git a/test/object-store/sync/file.cpp b/test/object-store/sync/file.cpp index 4d9cf0b1fd5..d7025b38c20 100644 --- a/test/object-store/sync/file.cpp +++ b/test/object-store/sync/file.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -132,8 +133,7 @@ TEST_CASE("sync_file: URL manipulation APIs", "[sync][file]") { } TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { - TestSyncManager tsm; - + realm::test_util::TestDirGuard test_dir(make_temp_dir(), false); const std::string identity = "abcdefghi"; const std::vector legacy_identities = {"legacy1", "legacy2"}; const auto& local_identity = legacy_identities[0]; @@ -141,10 +141,14 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { const std::string partition_str = random_string(10); const std::string partition = bson::Bson(partition_str).to_string(); const std::string expected_clean_app_id = "test_app_id%2A%24%23%40%21%251"; - const auto manager_base_path = fs::path{tsm.base_file_path()}.make_preferred() / "file-manager"; + const auto manager_base_path = fs::path{test_dir.c_str()}.make_preferred() / "file-manager"; util::try_make_dir(manager_base_path.string()); - const auto manager_path = manager_base_path / "mongodb-realm" / expected_clean_app_id; - auto manager = SyncFileManager(manager_base_path.string(), app_id); + const auto manager_path = manager_base_path / "mongodb-realm" / expected_clean_app_id / ""; + app::AppConfig config; + config.app_id = app_id; + config.base_file_path = manager_base_path; + auto manager = SyncFileManager(config); + REQUIRE(manager.app_path() == manager_path); SECTION("Realm path APIs") { auto relative_path = "s_" + partition_str; @@ -186,8 +190,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.fallback_hashed_path); REQUIRE(File::exists(expected_paths.fallback_hashed_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.fallback_hashed_path}); - REQUIRE(!File::exists(expected_paths.fallback_hashed_path)); } SECTION("legacy local identity path is detected and used") { @@ -203,8 +205,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.legacy_local_id_path); REQUIRE(File::exists(expected_paths.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.legacy_local_id_path}); - REQUIRE(!File::exists(expected_paths.legacy_local_id_path)); } SECTION("multiple legacy local identities are supported") { @@ -225,8 +225,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths_2.legacy_local_id_path); REQUIRE(File::exists(expected_paths_2.legacy_local_id_path)); REQUIRE(!File::exists(expected_paths_2.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths_2.legacy_local_id_path}); - REQUIRE(!File::exists(expected_paths_2.legacy_local_id_path)); } SECTION("legacy sync paths are detected and used") { @@ -242,8 +240,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual == expected_paths.legacy_sync_path); REQUIRE(File::exists(expected_paths.legacy_sync_path)); REQUIRE(!File::exists(expected_paths.current_preferred_path)); - manager.remove_user_realms(identity, {expected_paths.legacy_sync_path}); - REQUIRE(!File::exists(expected_paths.legacy_sync_path)); } SECTION("paths have a fallback hashed location if the preferred path is too long") { @@ -253,8 +249,6 @@ TEST_CASE("sync_file: SyncFileManager APIs", "[sync][file]") { REQUIRE(actual.length() < 500); REQUIRE(create_dummy_realm(actual)); REQUIRE(File::exists(actual)); - manager.remove_user_realms(identity, {actual}); - REQUIRE(!File::exists(actual)); } } diff --git a/test/object-store/sync/flx_migration.cpp b/test/object-store/sync/flx_migration.cpp index a4fee2cdb9c..ab0489b74a5 100644 --- a/test/object-store/sync/flx_migration.cpp +++ b/test/object-store/sync/flx_migration.cpp @@ -125,8 +125,8 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config1(session.app(), partition1, server_app_config.schema); - SyncTestFile config2(session.app(), partition2, server_app_config.schema); + SyncTestFile config1(session.app()->current_user(), partition1, server_app_config.schema); + SyncTestFile config2(session.app()->current_user(), partition2, server_app_config.schema); // Fill some objects auto objects1 = fill_test_data(config1, partition1); // 5 objects starting at 1 @@ -243,7 +243,7 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas } { - SyncTestFile pbs_config(session.app(), partition1, server_app_config.schema); + SyncTestFile pbs_config(session.app()->current_user(), partition1, server_app_config.schema); auto pbs_realm = Realm::get_shared_realm(pbs_config); REQUIRE(!wait_for_upload(*pbs_realm)); @@ -252,7 +252,7 @@ TEST_CASE("Test server migration and rollback", "[sync][flx][flx migration][baas check_data(pbs_realm, true, false); } { - SyncTestFile pbs_config(session.app(), partition2, server_app_config.schema); + SyncTestFile pbs_config(session.app()->current_user(), partition2, server_app_config.schema); auto pbs_realm = Realm::get_shared_realm(pbs_config); REQUIRE(!wait_for_upload(*pbs_realm)); @@ -273,7 +273,7 @@ TEST_CASE("Test client migration and rollback", "[sync][flx][flx migration][baas }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -328,7 +328,7 @@ TEST_CASE("Test client migration and rollback with recovery", "[sync][flx][flx m }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::Recover; config.schema_version = 0; @@ -484,7 +484,7 @@ TEST_CASE("An interrupted migration or rollback can recover on the next session" }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -621,7 +621,7 @@ TEST_CASE("Update to native FLX after migration", "[sync][flx][flx migration][ba }; auto server_app_config = minimal_app_config("server_migrate_rollback", mig_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -742,7 +742,7 @@ TEST_CASE("New table is synced after migration", "[sync][flx][flx migration][baa const Schema two_obj_schema{obj1_schema, obj2_schema}; auto server_app_config = minimal_app_config("server_migrate_rollback", two_obj_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), partition, server_app_config.schema); + SyncTestFile config(session.app()->current_user(), partition, server_app_config.schema); config.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal; config.schema_version = 0; @@ -847,7 +847,7 @@ TEST_CASE("Async open + client reset", "[sync][flx][flx migration][baas]") { server_app_config.dev_mode_enabled = true; std::optional config; // destruct this after the sessions are torn down TestAppSession session(create_app(server_app_config)); - config.emplace(session.app(), partition, server_app_config.schema); + config.emplace(session.app()->current_user(), partition, server_app_config.schema); config->sync_config->client_resync_mode = ClientResyncMode::Recover; config->sync_config->notify_before_client_reset = [&](SharedRealm before) { logger_ptr->debug("notify_before_client_reset"); diff --git a/test/object-store/sync/flx_sync.cpp b/test/object-store/sync/flx_sync.cpp index d41a0f87f09..823739b4bb6 100644 --- a/test/object-store/sync/flx_sync.cpp +++ b/test/object-store/sync/flx_sync.cpp @@ -2613,7 +2613,7 @@ TEST_CASE("flx: subscriptions persist after closing/reopening", "[sync][flx][baa TEST_CASE("flx: no subscription store created for PBS app", "[sync][flx][baas]") { auto server_app_config = minimal_app_config("flx_connect_as_pbs", g_minimal_schema); TestAppSession session(create_app(server_app_config)); - SyncTestFile config(session.app(), bson::Bson{}, g_minimal_schema); + SyncTestFile config(session.app()->current_user(), bson::Bson{}, g_minimal_schema); auto realm = Realm::get_shared_realm(config); CHECK(!wait_for_download(*realm)); @@ -2627,7 +2627,7 @@ TEST_CASE("flx: no subscription store created for PBS app", "[sync][flx][baas]") TEST_CASE("flx: connect to FLX as PBS returns an error", "[sync][flx][baas]") { FLXSyncTestHarness harness("connect_to_flx_as_pbs"); - SyncTestFile config(harness.app(), bson::Bson{}, harness.schema()); + SyncTestFile config(harness.app()->current_user(), bson::Bson{}, harness.schema()); std::mutex sync_error_mutex; util::Optional sync_error; config.sync_config->error_handler = [&](std::shared_ptr, SyncError error) mutable { @@ -2694,14 +2694,16 @@ TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][ auto transport = std::make_shared(); FLXSyncTestHarness harness("flx_wait_access_token2", FLXSyncTestHarness::default_server_schema(), transport); auto app = harness.app(); - std::shared_ptr user = app->current_user(); + std::shared_ptr user = app->current_user(); REQUIRE(user); REQUIRE(!user->access_token_refresh_required()); // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client. std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); using namespace std::chrono_literals; auto expires = std::chrono::system_clock::to_time_t(now - 30s); - user->update_access_token(encode_fake_jwt("fake_access_token", expires)); + user->update_data_for_testing([&](UserData& data) { + data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", expires)); + }); REQUIRE(user->access_token_refresh_required()); bool seen_waiting_for_access_token = false; @@ -2710,7 +2712,7 @@ TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][ transport->request_hook = [&](const Request&) { auto user = app->current_user(); REQUIRE(user); - for (auto& session : user->all_sessions()) { + for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) { if (session->state() == SyncSession::State::WaitingForAccessToken) { REQUIRE(!seen_waiting_for_access_token); seen_waiting_for_access_token = true; @@ -3438,7 +3440,7 @@ TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") { }}, }; - SyncTestFile config(harness->app(), Bson{}, schema); + SyncTestFile config(harness->app()->current_user(), Bson{}, schema); REQUIRE_EXCEPTION( Realm::get_shared_realm(config), SchemaValidationFailed, Catch::Matchers::ContainsSubstring("Asymmetric table 'Asymmetric2' not allowed in partition based sync")); @@ -3529,8 +3531,9 @@ TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") { Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}})); Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}})); realm->commit_transaction(); - - auto docs = harness.session().get_documents(*realm->config().sync_config->user, "Asymmetric", 2); + User* user = dynamic_cast(realm->config().sync_config->user.get()); + REALM_ASSERT(user); + auto docs = harness.session().get_documents(*user, "Asymmetric", 2); check_document(docs, foo_obj_id, {{"location", "foo"}}); check_document(docs, bar_obj_id, {{"location", "bar"}}); }, diff --git a/test/object-store/sync/metadata.cpp b/test/object-store/sync/metadata.cpp index 65b76469992..5bf22499b56 100644 --- a/test/object-store/sync/metadata.cpp +++ b/test/object-store/sync/metadata.cpp @@ -16,422 +16,618 @@ // //////////////////////////////////////////////////////////////////////////// -#include -#include -#include - -#include -#include -#include +#include #include -#include -#include - +#include +#include +#include #include #include +#include +#include + #include using namespace realm; -using namespace realm::util; -using File = realm::util::File; -using SyncAction = SyncFileActionMetadata::Action; +using namespace realm::app; +using realm::util::File; + +namespace { +const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir"; +const std::string metadata_path = base_path + "/mongodb-realm/app%20id/server-utility/metadata/sync_metadata.realm"; +constexpr const char* user_id = "user_id"; +constexpr const char* device_id = "device_id"; +const auto access_token = encode_fake_jwt("access_token", 123, 456); +const auto refresh_token = encode_fake_jwt("refresh_token", 123, 456); + +std::shared_ptr get_metadata_realm() +{ + RealmConfig realm_config; + realm_config.automatic_change_notifications = false; + realm_config.path = metadata_path; + return Realm::get_shared_realm(std::move(realm_config)); +} +} // anonymous namespace + +namespace realm::app { +static std::ostream& operator<<(std::ostream& os, AppConfig::MetadataMode mode) +{ + switch (mode) { + case AppConfig::MetadataMode::InMemory: + os << "InMemory"; + break; + case AppConfig::MetadataMode::NoEncryption: + os << "NoEncryption"; + break; + case AppConfig::MetadataMode::Encryption: + os << "Encryption"; + break; + default: + os << "unknown"; + break; + } + return os; +} +} // namespace realm::app -static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir"; -static const std::string metadata_path = base_path + "/metadata.realm"; +using Strings = std::vector; -TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { +TEST_CASE("app metadata: common", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - - SECTION("can be properly constructed") { - const auto identity = "testcase1a"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token().empty()); - } - - SECTION("properly reflects updating state") { - const auto identity = "testcase1b"; - const std::string sample_token = "this_is_a_user_token"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - user_metadata->set_access_token(sample_token); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); - } - - SECTION("can be properly re-retrieved from the same manager") { - const auto identity = "testcase1c"; - const std::string sample_token = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - // Get a second instance of the user metadata for the same identity. - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - } - - SECTION("properly reflects changes across different instances") { - const auto identity = "testcase1d"; - const std::string sample_token_1 = "this_is_a_user_token"; - auto first = manager.get_or_make_user_metadata(identity); - auto second = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token_1); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token_1); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token_1); - // Set the state again. - const std::string sample_token_2 = "this_is_another_user_token"; - second->set_access_token(sample_token_2); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token_2); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token_2); - } - - SECTION("can be removed") { - const auto identity = "testcase1e"; - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->is_valid()); - user_metadata->remove(); - REQUIRE(!user_metadata->is_valid()); - } - - SECTION("respects make_if_absent flag set to false in constructor") { - const std::string sample_token = "this_is_a_user_token"; - - SECTION("with no prior metadata for the identifier") { - const auto identity = "testcase1g1"; - auto user_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(!user_metadata); - } - SECTION("with valid prior metadata for the identifier") { - const auto identity = "testcase1g2"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->is_valid()); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - } - SECTION("with invalid prior metadata for the identifier") { - const auto identity = "testcase1g3"; - auto first = manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - first->set_state(SyncUser::State::Removed); - auto second = manager.get_or_make_user_metadata(identity, false); - REQUIRE(!second); - } + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = GENERATE(AppConfig::MetadataMode::InMemory, AppConfig::MetadataMode::NoEncryption); + config.base_file_path = base_path; + SyncFileManager file_manager(config); + auto store = create_metadata_store(config, file_manager); + + INFO(config.metadata_mode); + + SECTION("create_user() creates new logged-in users") { + REQUIRE_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + REQUIRE(store->has_logged_in_user(user_id)); + auto data = store->get_user(user_id); + REQUIRE(data); + REQUIRE(data->access_token.token == access_token); + REQUIRE(data->refresh_token.token == refresh_token); + REQUIRE(data->device_id == device_id); } -} -TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - const std::string provider_type = "https://realm.example.org"; + SECTION("passing malformed tokens create_user() results in a logged out user") { + store->create_user(user_id, refresh_token, "not a token", device_id); + auto data = store->get_user(user_id); + REQUIRE(data); + REQUIRE(data->access_token.token == ""); + REQUIRE(data->refresh_token.token == ""); + REQUIRE(data->device_id == device_id); + } - SECTION("properly list all marked and unmarked users") { - const auto identity1 = "testcase2a1"; - const auto identity2 = "testcase2a3"; - auto first = manager.get_or_make_user_metadata(identity1); - auto second = manager.get_or_make_user_metadata(identity1); - auto third = manager.get_or_make_user_metadata(identity2); - auto unmarked_users = manager.all_unmarked_users(); - REQUIRE(unmarked_users.size() == 2); - REQUIRE(results_contains_user(unmarked_users, identity1)); - REQUIRE(results_contains_user(unmarked_users, identity2)); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Now, mark a few users for removal. - first->set_state(SyncUser::State::Removed); - unmarked_users = manager.all_unmarked_users(); - REQUIRE(unmarked_users.size() == 1); - REQUIRE(results_contains_user(unmarked_users, identity2)); - marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 1); - REQUIRE(results_contains_user(marked_users, identity1)); + SECTION("create_user() marks the new user as the current user if it was created") { + CHECK(store->get_current_user() == ""); + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->get_current_user() == user_id); + store->create_user("user 2", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); } -} -TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - - const std::string local_uuid_1 = "asdfg"; - const std::string local_uuid_2 = "qwerty"; - const std::string url_1 = "realm://realm.example.com/1"; - const std::string url_2 = "realm://realm.example.com/2"; - - SECTION("can be properly constructed") { - const auto original_name = util::make_temp_dir() + "foobar/test1"; - manager.make_file_action_metadata(original_name, SyncAction::BackUpThenDeleteRealm); - auto metadata = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata.original_name() == original_name); - REQUIRE(metadata.new_name() == none); - REQUIRE(metadata.action() == SyncAction::BackUpThenDeleteRealm); - } - - SECTION("properly reflects updating state, across multiple instances") { - const auto original_name = util::make_temp_dir() + "foobar/test2a"; - const std::string new_name_1 = util::make_temp_dir() + "foobar/test2b"; - const std::string new_name_2 = util::make_temp_dir() + "foobar/test2c"; - - manager.make_file_action_metadata(original_name, SyncAction::BackUpThenDeleteRealm, new_name_1); - auto metadata_1 = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata_1.original_name() == original_name); - REQUIRE(metadata_1.new_name() == new_name_1); - REQUIRE(metadata_1.action() == SyncAction::BackUpThenDeleteRealm); - - manager.make_file_action_metadata(original_name, SyncAction::DeleteRealm, new_name_2); - auto metadata_2 = *manager.get_file_action_metadata(original_name); - REQUIRE(metadata_1.original_name() == original_name); - REQUIRE(metadata_1.new_name() == new_name_2); - REQUIRE(metadata_1.action() == SyncAction::DeleteRealm); - REQUIRE(metadata_2.original_name() == original_name); - REQUIRE(metadata_2.new_name() == new_name_2); - REQUIRE(metadata_2.action() == SyncAction::DeleteRealm); + SECTION("create_user() only updates the given fields and leaves the rest unchanged") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto data = store->get_user(user_id); + REQUIRE(data); + data->profile = bson::BsonDocument{{"name", "user's name"}, {"email", "user's email"}}; + data->identities = {{"identity", "provider"}}; + store->update_user(user_id, *data); + + const auto access_token_2 = encode_fake_jwt("access_token_2", 123, 456); + const auto refresh_token_2 = encode_fake_jwt("refresh_token_2", 123, 456); + store->create_user(user_id, refresh_token_2, access_token_2, "device id 2"); + + auto data2 = store->get_user(user_id); + REQUIRE(data2); + CHECK(data2->access_token.token == access_token_2); + CHECK(data2->refresh_token.token == refresh_token_2); + CHECK(data2->legacy_identities.empty()); + CHECK(data2->device_id == "device id 2"); + CHECK(data2->identities == data->identities); + CHECK(data2->profile.data() == data->profile.data()); + } + + SECTION("has_logged_in_user() is only true if user is present and valid") { + CHECK_FALSE(store->has_logged_in_user("")); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, "malformed token", device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, "", device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, "malformed token", access_token, device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, "", access_token, device_id); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::LoggedOut); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::Removed); + CHECK_FALSE(store->has_logged_in_user(user_id)); + + store->create_user(user_id, refresh_token, access_token, device_id); + CHECK(store->has_logged_in_user(user_id)); + CHECK_FALSE(store->has_logged_in_user("")); + CHECK_FALSE(store->has_logged_in_user("different user")); + } + + SECTION("get_all_users() returns all non-removed users") { + store->create_user("user 1", refresh_token, access_token, device_id); + store->create_user("user 2", refresh_token, access_token, device_id); + store->create_user("user 3", refresh_token, access_token, device_id); + store->create_user("user 4", refresh_token, access_token, device_id); + + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3", "user 4"}); + + store->log_out("user 2", SyncUser::State::LoggedOut); + store->delete_user(file_manager, "user 4"); + + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->create_user("user 1", "", access_token, device_id); + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->create_user("user 3", refresh_token, "", device_id); + CHECK(store->get_all_users() == Strings{"user 1", "user 2", "user 3"}); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(!store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + + store->delete_user(file_manager, "user 1"); + store->delete_user(file_manager, "user 2"); + store->delete_user(file_manager, "user 3"); + store->delete_user(file_manager, "user 4"); + CHECK(store->get_all_users().empty()); + CHECK(!store->has_logged_in_user("user 1")); + CHECK(!store->has_logged_in_user("user 2")); + CHECK(!store->has_logged_in_user("user 3")); + CHECK(!store->has_logged_in_user("user 4")); + } + + SECTION("set_current_user() sets to the requested user") { + CHECK(store->get_current_user() == ""); + store->create_user("user 1", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 1"); + store->create_user("user 2", refresh_token, access_token, device_id); + CHECK(store->get_current_user() == "user 2"); + + store->set_current_user(""); + CHECK(store->get_current_user() == "user 1"); + store->set_current_user("user 2"); + CHECK(store->get_current_user() == "user 2"); + store->set_current_user("user 1"); + CHECK(store->get_current_user() == "user 1"); + } + + SECTION("current user falls back to the first valid one if current is invalid") { + store->create_user("user 1", refresh_token, access_token, device_id); + store->create_user("user 2", refresh_token, access_token, device_id); + store->create_user("user 3", refresh_token, access_token, device_id); + + auto data = store->get_user("user 3"); + data->access_token.token.clear(); + data->refresh_token.token.clear(); + store->update_user("user 3", *data); + CHECK(store->get_current_user() == "user 1"); + store->update_user("user 1", *data); + CHECK(store->get_current_user() == "user 2"); + + store->set_current_user("not a user"); + CHECK(store->get_current_user() == "user 2"); + store->set_current_user(""); + CHECK(store->get_current_user() == "user 2"); + } + + SECTION("log_out() updates the user state without deleting anything") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto path = File::resolve("file 1", base_path); + File(path, File::mode_Write); + CHECK(File::exists(path)); + store->add_realm_path(user_id, path); + store->add_realm_path(user_id, "invalid path"); + store->log_out(user_id, SyncUser::State::Removed); + CHECK(File::exists(path)); + } + + SECTION("delete_user() deletes the files recorded with add_realm_file_path()") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto path = File::resolve("file 1", base_path); + File(path, File::mode_Write); + CHECK(File::exists(path)); + store->add_realm_path(user_id, path); + store->add_realm_path(user_id, "invalid path"); + store->delete_user(file_manager, user_id); + CHECK_FALSE(File::exists(path)); + } + + SECTION("update_user() does not set legacy identities") { + store->create_user(user_id, refresh_token, access_token, device_id); + auto data = store->get_user(user_id); + data->legacy_identities.push_back("legacy uuid"); + store->update_user(user_id, *data); + data = store->get_user(user_id); + REQUIRE(data->legacy_identities.empty()); + } + + SECTION("immediately run nonexistent action") { + CHECK_FALSE(store->immediately_run_file_actions(file_manager, "invalid")); + } + + SECTION("immediately run DeleteRealm action") { + auto path = util::make_temp_file("delete-realm-action"); + store->create_file_action(SyncFileAction::DeleteRealm, path, {}); + CHECK(File::exists(path)); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } + + SECTION("immediately run BackUpThenDeleteRealm action") { + auto path = util::make_temp_file("delete-realm-action"); + auto backup_path = util::make_temp_file("backup-path"); + File::remove(backup_path); + store->create_file_action(SyncFileAction::BackUpThenDeleteRealm, path, backup_path); + CHECK(File::exists(path)); + CHECK(store->immediately_run_file_actions(file_manager, path)); + CHECK_FALSE(File::exists(path)); + CHECK(File::exists(backup_path)); + CHECK_FALSE(store->immediately_run_file_actions(file_manager, path)); + } + + SECTION("file actions replace existing ones for the same path") { + } + + SECTION("failed backup action is preserved") { + } + + SECTION("failed delete after backup succeeds turns into a delete action") { } } -TEST_CASE("sync_metadata: file action metadata APIs", "[sync][metadata]") { +TEST_CASE("app metadata: in memory", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::InMemory; + config.base_file_path = base_path; + SyncFileManager file_manager(config); - SyncMetadataManager manager(metadata_path, false); - SECTION("properly list all pending actions, reflecting their deletion") { - const auto filename1 = util::make_temp_dir() + "foobar/file1"; - const auto filename2 = util::make_temp_dir() + "foobar/file2"; - const auto filename3 = util::make_temp_dir() + "foobar/file3"; - manager.make_file_action_metadata(filename1, SyncAction::BackUpThenDeleteRealm); - manager.make_file_action_metadata(filename2, SyncAction::BackUpThenDeleteRealm); - manager.make_file_action_metadata(filename3, SyncAction::BackUpThenDeleteRealm); - auto actions = manager.all_pending_actions(); - REQUIRE(actions.size() == 3); - REQUIRE(results_contains_original_name(actions, filename1)); - REQUIRE(results_contains_original_name(actions, filename2)); - REQUIRE(results_contains_original_name(actions, filename3)); - manager.get_file_action_metadata(filename1)->remove(); - manager.get_file_action_metadata(filename2)->remove(); - manager.get_file_action_metadata(filename3)->remove(); - REQUIRE(actions.size() == 0); + SECTION("does not persist users between instances") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + } } } -TEST_CASE("sync_metadata: results", "[sync][metadata]") { +TEST_CASE("app metadata: persisted", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); - const auto identity1 = "testcase3a1"; - const auto identity2 = "testcase3a3"; - - SECTION("properly update as underlying items are added") { - auto results = manager.all_unmarked_users(); - REQUIRE(results.size() == 0); - // Add users, one at a time. - auto first = manager.get_or_make_user_metadata(identity1); - REQUIRE(results.size() == 1); - REQUIRE(results_contains_user(results, identity1)); - auto second = manager.get_or_make_user_metadata(identity2); - REQUIRE(results.size() == 2); - REQUIRE(results_contains_user(results, identity2)); - } - - SECTION("properly update as underlying items are removed") { - auto results = manager.all_unmarked_users(); - auto first = manager.get_or_make_user_metadata(identity1); - auto second = manager.get_or_make_user_metadata(identity2); - REQUIRE(results.size() == 2); - REQUIRE(results_contains_user(results, identity1)); - REQUIRE(results_contains_user(results, identity2)); - // Remove users, one at a time. - first->remove(); - REQUIRE(results.size() == 1); - REQUIRE(!results_contains_user(results, identity1)); - second->remove(); - REQUIRE(results.size() == 0); + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.base_file_path = base_path; + SyncFileManager file_manager(config); + + SECTION("persists users between instances") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + store->log_out(user_id, SyncUser::State::LoggedOut); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + CHECK(store->get_all_users() == Strings{user_id}); + } } -} -TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync][metadata]") { - test_util::TestDirGuard temp_dir(base_path); - - SECTION("works for the basic case") { - const auto identity = "testcase4a"; - const std::string provider_type = "any-type"; - const std::string sample_token = "this_is_a_user_token"; - SyncMetadataManager first_manager(metadata_path, false); - auto first = first_manager.get_or_make_user_metadata(identity); - first->set_access_token(sample_token); - REQUIRE(first->identity() == identity); - REQUIRE(first->access_token() == sample_token); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - first->set_state(SyncUser::State::LoggedOut); - - SyncMetadataManager second_manager(metadata_path, false); - auto second = second_manager.get_or_make_user_metadata(identity, false); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == sample_token); - REQUIRE(second->state() == SyncUser::State::LoggedOut); + SECTION("can read legacy identities if present") { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + + auto data = store->get_user(user_id); + CHECK(data->legacy_identities.empty()); + + { + // Add some legacy uuids by modifying the underlying realm directly + auto realm = get_metadata_realm(); + auto table = realm->read_group().get_table("class_UserMetadata"); + REQUIRE(table); + REQUIRE(table->size() == 1); + auto list = table->begin()->get_list("legacy_uuids"); + realm->begin_transaction(); + list.add("uuid 1"); + list.add("uuid 2"); + realm->commit_transaction(); + } + + data = store->get_user(user_id); + CHECK(data->legacy_identities == std::vector{"uuid 1", "uuid 2"}); } -} -TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { - test_util::TestDirGuard test_dir(base_path); - const auto identity0 = "identity0"; - SECTION("prohibits opening the metadata Realm with different keys") { - SECTION("different keys") { - { - // Open metadata realm, make metadata - std::vector key0 = make_test_encryption_key(10); - SyncMetadataManager manager0(metadata_path, true, key0); - - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); - REQUIRE(bool(user_metadata0)); - CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->access_token().empty()); - CHECK(user_metadata0->is_valid()); - } - // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope - // Open new metadata realm at path with different key - std::vector key1 = make_test_encryption_key(11); - SyncMetadataManager manager1(metadata_path, true, key1); - - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); - // Expect previous metadata to have been deleted - CHECK_FALSE(bool(user_metadata1)); - - // But new metadata can still be created - const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); - CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->access_token().empty()); - CHECK(user_metadata2->is_valid()); + SECTION("runs file actions on creation") { + auto path = util::make_temp_file("file_to_delete"); + auto nonexistent = util::make_temp_file("nonexistent"); + File::remove(nonexistent); + + { + auto store = create_metadata_store(config, file_manager); + store->create_file_action(SyncFileAction::DeleteRealm, path, ""); + store->create_file_action(SyncFileAction::DeleteRealm, nonexistent, ""); } - SECTION("different encryption settings") { - { - // Encrypt metadata realm at path, make metadata - SyncMetadataManager manager0(metadata_path, true, make_test_encryption_key(10)); - - auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); - REQUIRE(bool(user_metadata0)); - CHECK(user_metadata0->identity() == identity0); - CHECK(user_metadata0->access_token().empty()); - CHECK(user_metadata0->is_valid()); - } - // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope - // Open new metadata realm at path with different encryption configuration - SyncMetadataManager manager1(metadata_path, false); - auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); - // Expect previous metadata to have been deleted - CHECK_FALSE(bool(user_metadata1)); - - // But new metadata can still be created - const auto identity1 = "identity1"; - auto user_metadata2 = manager1.get_or_make_user_metadata(identity1); - CHECK(user_metadata2->identity() == identity1); - CHECK(user_metadata2->access_token().empty()); - CHECK(user_metadata2->is_valid()); + + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + REQUIRE_FALSE(File::exists(nonexistent)); + + // Check the underlying realm to verify both file actions are gone + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_FileActionMetadata")->is_empty()); + } + + SECTION("deletes data for removed users on creation") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->log_out(user_id, SyncUser::State::Removed); + } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->get_all_users().empty()); } + // Check the underlying realm as removed users aren't exposed in the API + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty()); + } + + SECTION("deletes realm files for removed users on creation") { + auto path = util::make_temp_file("file_to_delete"); + auto nonexistent = util::make_temp_file("nonexistent"); + REQUIRE(File::exists(path)); + File::remove(nonexistent); + + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, nonexistent); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); + } + + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + REQUIRE_FALSE(File::exists(nonexistent)); } - SECTION("works when enabled") { - std::vector key = make_test_encryption_key(10); - const auto identity = "testcase5a"; - SyncMetadataManager manager(metadata_path, true, key); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(bool(user_metadata)); - CHECK(user_metadata->identity() == identity); - CHECK(user_metadata->access_token().empty()); - CHECK(user_metadata->is_valid()); - // Reopen the metadata file with the same key. - SyncMetadataManager manager_2(metadata_path, true, key); - auto user_metadata_2 = manager_2.get_or_make_user_metadata(identity, false); - REQUIRE(bool(user_metadata_2)); - CHECK(user_metadata_2->identity() == identity); - CHECK(user_metadata_2->is_valid()); - } - - SECTION("enabled without custom encryption key") { #if REALM_PLATFORM_APPLE - static bool can_access_keychain = [] { - bool can_access = keychain::create_new_metadata_realm_key() != none; - if (!can_access) { - std::cout << "Skipping keychain tests as the keychain is not accessible\n"; - } - return can_access; - }(); - if (!can_access_keychain) { - return; + SECTION("continues tracking files to delete if deletion fails") { + auto path = util::make_temp_file("file_to_delete"); + REQUIRE(File::exists(path)); + + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); } - auto delete_key = util::make_scope_exit([=]() noexcept { - keychain::delete_metadata_realm_encryption_key(); - }); - SECTION("automatically generates an encryption key for new files") { - { - SyncMetadataManager manager(metadata_path, true, none); - manager.set_current_user_identity(identity0); - } + REQUIRE(chflags(path.c_str(), UF_IMMUTABLE) == 0); + create_metadata_store(config, file_manager); + REQUIRE(File::exists(path)); + REQUIRE(chflags(path.c_str(), 0) == 0); + create_metadata_store(config, file_manager); + REQUIRE_FALSE(File::exists(path)); + } +#endif - // Should be able to reopen and read data - { - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == identity0); - } + SECTION("stops tracking files if it no longer exists") { + auto path = util::make_temp_file("nonexistent"); + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + store->add_realm_path(user_id, path); + store->log_out(user_id, SyncUser::State::Removed); + } + + File::remove(path); + create_metadata_store(config, file_manager); + auto realm = get_metadata_realm(); + CHECK(realm->read_group().get_table("class_UserMetadata")->is_empty()); + } + + SECTION("deletes legacy untracked files") { + } +} + +TEST_CASE("app metadata: encryption", "[sync][metadata]") { + test_util::TestDirGuard test_dir(base_path); - // Verify that the file is actually encrypted - REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, - Catch::Matchers::ContainsSubstring("invalid mnemonic")); + + AppConfig config; + config.app_id = "app id"; + config.metadata_mode = AppConfig::MetadataMode::Encryption; + config.custom_encryption_key = make_test_encryption_key(10); + config.base_file_path = base_path; + SyncFileManager file_manager(config); + + // Verify that the Realm is actually encrypted with the expected key + auto open_realm_with_key = [](auto& key) { + RealmConfig realm_config; + realm_config.automatic_change_notifications = false; + realm_config.path = metadata_path; + // sanity check that using the wrong key throws, as otherwise we'd pass + // if we were checking the wrong path + realm_config.encryption_key = make_test_encryption_key(0); + CHECK_THROWS(Realm::get_shared_realm(realm_config)); + + if (key) { + realm_config.encryption_key = *key; + } + else { + realm_config.encryption_key.clear(); } + CHECK_NOTHROW(Realm::get_shared_realm(realm_config)); + }; - SECTION("leaves existing unencrypted files unencrypted") { - { - SyncMetadataManager manager(metadata_path, false, none); - manager.set_current_user_identity(identity0); - } - { - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == identity0); - } - REQUIRE_NOTHROW(Group(metadata_path)); + SECTION("can open and reopen with an explicit key") { + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); } + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(config.custom_encryption_key); + } - SECTION("recreates the file if the old encryption key was lost") { - { - SyncMetadataManager manager(metadata_path, true, none); - manager.set_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); + + // Change to new encryption key + { + config.custom_encryption_key = make_test_encryption_key(11); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + // Change to unencrypted + { + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + config.custom_encryption_key.reset(); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + + // Change back to encrypted + { + config.metadata_mode = AppConfig::MetadataMode::Encryption; + config.custom_encryption_key = make_test_encryption_key(12); + auto store = create_metadata_store(config, file_manager); + CHECK_FALSE(store->has_logged_in_user(user_id)); + store->create_user(user_id, refresh_token, access_token, device_id); + } + open_realm_with_key(config.custom_encryption_key); + } + +#if REALM_PLATFORM_APPLE + static bool can_access_keychain = [] { + bool can_access = keychain::create_new_metadata_realm_key() != none; + if (can_access) { keychain::delete_metadata_realm_encryption_key(); + } + else { + std::cout << "Skipping keychain tests as the keychain is not accessible\n"; + } + return can_access; + }(); + if (!can_access_keychain) { + return; + } + auto delete_key = util::make_scope_exit([=]() noexcept { + keychain::delete_metadata_realm_encryption_key(); + }); - { - // File should now be missing the data - SyncMetadataManager manager(metadata_path, true, none); - REQUIRE(manager.get_current_user_identity() == none); - } - // New file should be encrypted - REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, - Catch::Matchers::ContainsSubstring("invalid mnemonic")); + SECTION("encryption key is automatically generated and stored for new files") { + config.custom_encryption_key.reset(); + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); } -#else - REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, true, none), InvalidArgument, + auto key = keychain::get_existing_metadata_realm_key(); + REQUIRE(key); + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(key); + } + + SECTION("existing unencrypted files are left unencrypted") { + config.custom_encryption_key.reset(); + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + { + auto store = create_metadata_store(config, file_manager); + store->create_user(user_id, refresh_token, access_token, device_id); + } + + config.metadata_mode = AppConfig::MetadataMode::Encryption; + { + auto store = create_metadata_store(config, file_manager); + CHECK(store->has_logged_in_user(user_id)); + } + open_realm_with_key(config.custom_encryption_key); + } +#else // REALM_PLATFORM_APPLE + SECTION("requires an explicit encryption key") { + config.custom_encryption_key.reset(); + REQUIRE_EXCEPTION(create_metadata_store(config, file_manager), InvalidArgument, "Metadata Realm encryption was specified, but no encryption key was provided."); -#endif } +#endif // REALM_PLATFORM_APPLE } #ifndef SWIFT_PACKAGE // The SPM build currently doesn't copy resource files TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); + util::make_dir_recursive(File::parent_dir(metadata_path)); + const std::string provider_type = "https://realm.example.org"; const auto identity = "metadata migration test"; - const std::string sample_token = "metadata migration token"; + const std::string sample_token = encode_fake_jwt("metadata migration token", 456, 123); const auto access_token_1 = encode_fake_jwt("access token 1", 456, 123); const auto access_token_2 = encode_fake_jwt("access token 2", 456, 124); const auto refresh_token_1 = encode_fake_jwt("refresh token 1", 456, 123); const auto refresh_token_2 = encode_fake_jwt("refresh token 2", 456, 124); + AppConfig config; + config.app_id = "app id"; + config.base_file_path = base_path; + config.metadata_mode = AppConfig::MetadataMode::NoEncryption; + SyncFileManager file_manager(config); + + // change to true to create a test file for the current schema version // this will only work on unix-like systems if ((false)) { @@ -457,7 +653,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { user->set_identities({{"identity 1", "a"}, {"shared identity", "shared"}}); user->add_realm_file_path("file 1"); user->add_realm_file_path("file 2"); - + user = manager.get_or_make_user_metadata(name, "b"); user->set_state_and_tokens(state2, token_2, refresh_token_2); user->set_identities({{"identity 2", "b"}, {"shared identity", "shared"}}); @@ -496,16 +692,13 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { } #else { // Create a metadata Realm with a test user - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - user_metadata->set_access_token(sample_token); + auto store = create_metadata_store(config, file_manager); + store->create_user(identity, sample_token, sample_token, "device id"); } #endif // Open the metadata Realm directly and grab the schema version from it - Realm::Config config; - config.path = metadata_path; - auto realm = Realm::get_shared_realm(config); + auto realm = get_metadata_realm(); realm->read_group(); auto schema_version = realm->schema_version(); @@ -528,53 +721,45 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 4") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v4.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); + auto store = create_metadata_store(config, file_manager); + auto user_metadata = store->get_user(identity); + REQUIRE(user_metadata->access_token.token == sample_token); } SECTION("open schema version 5") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v5.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); - auto user_metadata = manager.get_or_make_user_metadata(identity); - REQUIRE(user_metadata->identity() == identity); - REQUIRE(user_metadata->access_token() == sample_token); + auto store = create_metadata_store(config, file_manager); + auto user_metadata = store->get_user(identity); + REQUIRE(user_metadata->access_token.token == sample_token); } SECTION("open schema version 6") { - using State = SyncUser::State; File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); + auto store = create_metadata_store(config, file_manager); - SyncUserIdentity id_1{"identity 1", "a"}; - SyncUserIdentity id_2{"identity 2", "b"}; - SyncUserIdentity id_shared{"shared identity", "shared"}; - const std::vector all_ids = {id_1, id_shared, id_2}; + UserIdentity id_1{"identity 1", "a"}; + UserIdentity id_2{"identity 2", "b"}; + UserIdentity id_shared{"shared identity", "shared"}; + const std::vector all_ids = {id_1, id_shared, id_2}; const std::vector realm_files = {"file 1", "file 2", "file 3"}; - auto check_user = [&](const char* user_id, State state, const std::string& access_token, - const std::string& refresh_token, const std::vector& uuids) { - auto user = manager.get_or_make_user_metadata(user_id, false); + auto check_user = [&](const char* user_id, const std::string& access_token, const std::string& refresh_token, + const std::vector& uuids) { + auto user = store->get_user(user_id); CAPTURE(user_id); - REQUIRE(user); - CHECK(user->state() == state); - CHECK(user->access_token() == access_token); - CHECK(user->refresh_token() == refresh_token); - CHECK(user->legacy_identities() == uuids); - CHECK(user->identities() == all_ids); - CHECK(user->realm_file_paths() == realm_files); + CHECK(user->access_token.token == access_token); + CHECK(user->refresh_token.token == refresh_token); + CHECK(user->legacy_identities == uuids); + CHECK(user->identities == all_ids); }; - REQUIRE_FALSE(manager.get_or_make_user_metadata("removed user", false)); - check_user("first logged in, second logged out", State::LoggedIn, access_token_1, refresh_token_1, - {"1", "2"}); - check_user("first logged in, second removed", State::LoggedIn, access_token_1, refresh_token_1, {"3", "4"}); - check_user("second logged in, first logged out", State::LoggedIn, access_token_2, refresh_token_2, - {"5", "6"}); - check_user("second logged in, first removed", State::LoggedIn, access_token_2, refresh_token_2, {"7", "8"}); - check_user("both logged in, first newer", State::LoggedIn, access_token_2, refresh_token_1, {"9", "10"}); - check_user("both logged in, second newer", State::LoggedIn, access_token_2, refresh_token_2, {"11", "12"}); + REQUIRE_FALSE(store->has_logged_in_user("removed user")); + check_user("first logged in, second logged out", access_token_1, refresh_token_1, {"1", "2"}); + check_user("first logged in, second removed", access_token_1, refresh_token_1, {"3", "4"}); + check_user("second logged in, first logged out", access_token_2, refresh_token_2, {"5", "6"}); + check_user("second logged in, first removed", access_token_2, refresh_token_2, {"7", "8"}); + check_user("both logged in, first newer", access_token_2, refresh_token_1, {"9", "10"}); + check_user("both logged in, second newer", access_token_2, refresh_token_2, {"11", "12"}); } } #endif // SWIFT_PACKAGE diff --git a/test/object-store/sync/session/session.cpp b/test/object-store/sync/session/session.cpp index ad2a3e6221c..dba6b020839 100644 --- a/test/object-store/sync/session/session.cpp +++ b/test/object-store/sync/session/session.cpp @@ -51,8 +51,8 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { auto& server = tsm.sync_server(); const std::string realm_base_url = server.base_url(); - auto check_for_sessions = [](SyncUser& user, size_t count, SyncSession::State state) { - auto sessions = user.all_sessions(); + auto check_for_sessions = [](TestUser& user, size_t count, SyncSession::State state) { + auto sessions = user.sync_manager()->get_all_sessions_for(user); CHECK(sessions.size() == count); for (auto& session : sessions) { CHECK(session->state() == state); @@ -69,9 +69,9 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { // Check the sessions on the SyncUser. check_for_sessions(*user, 2, SyncSession::State::Active); - auto s1 = user->session_for_on_disk_path(session1->path()); + auto s1 = tsm.sync_manager()->get_existing_session(session1->path()); REQUIRE(s1 == session1); - auto s2 = user->session_for_on_disk_path(session2->path()); + auto s2 = tsm.sync_manager()->get_existing_session(session2->path()); REQUIRE(s2 == session2); } @@ -88,7 +88,7 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { EventLoop::main().run_until([&] { return sessions_are_inactive(*session1, *session2); }); - check_for_sessions(*user, 0, SyncSession::State::Inactive); + check_for_sessions(*user, 2, SyncSession::State::Inactive); } SECTION("a SyncUser defers binding new sessions until it is logged in") { @@ -101,8 +101,9 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { spin_runloop(); REQUIRE(session1->state() == SyncSession::State::Inactive); REQUIRE(session2->state() == SyncSession::State::Inactive); - check_for_sessions(*user, 0, SyncSession::State::Inactive); - user->log_in(ENCODE_FAKE_JWT("fake_access_token"), ENCODE_FAKE_JWT("fake_refresh_token")); + check_for_sessions(*user, 2, SyncSession::State::Inactive); + // Log the user back in via the sync manager. + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session1, *session2); }); @@ -118,14 +119,16 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { return sessions_are_active(*session1, *session2); }); check_for_sessions(*user, 2, SyncSession::State::Active); + // Log the user out. user->log_out(); REQUIRE(user->state() == State::LoggedOut); // Run the runloop many iterations to see if the sessions spuriously rebind. spin_runloop(); REQUIRE(session1->state() == SyncSession::State::Inactive); REQUIRE(session2->state() == SyncSession::State::Inactive); - check_for_sessions(*user, 0, SyncSession::State::Inactive); - user->log_in(ENCODE_FAKE_JWT("fake_access_token"), ENCODE_FAKE_JWT("fake_refresh_token")); + check_for_sessions(*user, 2, SyncSession::State::Inactive); + // Log the user back in via the sync manager. + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session1, *session2); }); @@ -158,7 +161,7 @@ TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") { auto session = sync_session( user, path, [](auto, auto) {}, SyncSessionStopPolicy::Immediately, &on_disk_path); CHECK(session); - session = user->session_for_on_disk_path(on_disk_path); + session = tsm.sync_manager()->get_existing_session(on_disk_path); CHECK(session); } @@ -427,7 +430,8 @@ TEST_CASE("sync: error handling", "[sync][session]") { } SECTION("Properly handles a client reset error") { - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto session = sync_session(user, "/test", store_sync_error); std::string on_disk_path = session->path(); @@ -456,7 +460,9 @@ TEST_CASE("sync: error handling", "[sync][session]") { std::string recovery_path = error->user_info[SyncError::c_recovery_file_path_key]; auto idx = recovery_path.find("recovered_realm"); CHECK(idx != std::string::npos); - idx = recovery_path.find(tsm.sync_manager()->recovery_directory_path()); + idx = recovery_path.find(oas.app()->config().base_file_path); + CHECK(idx != std::string::npos); + idx = recovery_path.find(oas.app()->config().app_id); CHECK(idx != std::string::npos); if (just_before.tm_year == just_after.tm_year) { idx = recovery_path.find(util::format_local_time(just_after_raw, "%Y")); diff --git a/test/object-store/sync/session/wait_for_completion.cpp b/test/object-store/sync/session/wait_for_completion.cpp index ae2923b5eae..4ad9daa0bad 100644 --- a/test/object-store/sync/session/wait_for_completion.cpp +++ b/test/object-store/sync/session/wait_for_completion.cpp @@ -72,8 +72,7 @@ TEST_CASE("SyncSession: wait_for_download_completion() API", "[sync][pbs][sessio spin_runloop(); REQUIRE(handler_called == false); // Log the user back in - user = sync_manager->get_user(user->identity(), ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), ""); + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); @@ -111,10 +110,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] if (!EventLoop::has_implementation()) return; - TestSyncManager::Config config; - config.should_teardown_test_directory = false; - SyncServer::Config server_config = {false}; - TestSyncManager tsm(config, server_config); + TestSyncManager tsm({}, {false}); auto& server = tsm.sync_server(); auto sync_manager = tsm.sync_manager(); std::atomic handler_called(false); @@ -154,8 +150,7 @@ TEST_CASE("SyncSession: wait_for_upload_completion() API", "[sync][pbs][session] spin_runloop(); REQUIRE(handler_called == false); // Log the user back in - user = sync_manager->get_user(user->identity(), ENCODE_FAKE_JWT("not_a_real_token"), - ENCODE_FAKE_JWT("not_a_real_token"), ""); + user->log_in(); EventLoop::main().run_until([&] { return sessions_are_active(*session); }); diff --git a/test/object-store/sync/sync_manager.cpp b/test/object-store/sync/sync_manager.cpp index 57bb908cbc1..3aefafcf347 100644 --- a/test/object-store/sync/sync_manager.cpp +++ b/test/object-store/sync/sync_manager.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -34,63 +35,40 @@ using namespace realm; using namespace realm::util; using File = realm::util::File; +using MetadataMode = app::AppConfig::MetadataMode; static const auto base_path = fs::path{util::make_temp_dir()}.make_preferred() / "realm_objectstore_sync_manager.test-dir"; static const std::string dummy_device_id = "123400000000000000000000"; -namespace { -bool validate_user_in_vector(std::vector> vector, const std::string& identity, - const std::string& refresh_token, const std::string& access_token, - const std::string& device_id) -{ - for (auto& user : vector) { - if (user->identity() == identity && user->refresh_token() == refresh_token && - user->access_token() == access_token && user->has_device_id() && user->device_id() == device_id) { - return true; - } - } - return false; -} -} // anonymous namespace - -TEST_CASE("sync_manager: basic properties and APIs", "[sync][sync manager]") { - TestSyncManager tsm; - - SECTION("should not crash on 'reconnect()'") { - tsm.sync_manager()->reconnect(); - } -} - -TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { +TEST_CASE("App: path_for_realm API", "[sync][app][file]") { const std::string raw_url = "realms://realm.example.org/a/b/~/123456/xyz"; SECTION("should work properly") { - TestSyncManager tsm; - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto base_path = - fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->identity(); + fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->user_id(); const auto expected = base_path / "realms%3A%2F%2Frealm.example.org%2Fa%2Fb%2F%7E%2F123456%2Fxyz.realm"; - SyncConfig config(user, bson::Bson{}); - REQUIRE(tsm.sync_manager()->path_for_realm(config, raw_url) == expected); + SyncConfig sync_config(user, bson::Bson{}); + REQUIRE(oas.app()->path_for_realm(sync_config, raw_url) == expected); // This API should also generate the directory if it doesn't already exist. REQUIRE_DIR_PATH_EXISTS(base_path); } SECTION("should produce the expected path for all partition key types") { - TestSyncManager tsm; - auto sync_manager = tsm.sync_manager(); - auto user = tsm.fake_user(); + OfflineAppSession oas; + auto user = oas.make_user(); auto base_path = - fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->identity(); - + fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id" / user->user_id(); + auto app = oas.app(); // Directory should not be created until we get the path REQUIRE_DIR_PATH_DOES_NOT_EXIST(base_path); SECTION("string") { const bson::Bson partition("string-partition-value&^#"); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "s_string-partition-value%26%5E%23.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "s_string-partition-value%26%5E%23.realm"); } SECTION("string which exceeds the file system path length limit") { @@ -100,9 +78,9 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { SyncConfig config(user, partition); // Note: does not include `identity` as that's in the hashed part - auto base_path = fs::path{tsm.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id"; + auto base_path = fs::path{oas.base_file_path()}.make_preferred() / "mongodb-realm" / "app_id"; const std::string expected_suffix = ".realm"; - std::string actual = sync_manager->path_for_realm(config); + std::string actual = oas.app()->path_for_realm(config); size_t expected_length = base_path.string().length() + 1 + 64 + expected_suffix.length(); REQUIRE(actual.length() == expected_length); REQUIRE(StringData(actual).begins_with(base_path.string())); @@ -112,66 +90,65 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { SECTION("int32") { const bson::Bson partition(int32_t(-25)); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "i_-25.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "i_-25.realm"); } SECTION("int64") { const bson::Bson partition(int64_t(1.15e18)); // > 32 bits SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "l_1150000000000000000.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "l_1150000000000000000.realm"); } SECTION("UUID") { const bson::Bson partition(UUID("3b241101-e2bb-4255-8caf-4136c566a961")); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == - base_path / "u_3b241101-e2bb-4255-8caf-4136c566a961.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "u_3b241101-e2bb-4255-8caf-4136c566a961.realm"); } SECTION("ObjectId") { const bson::Bson partition(ObjectId("0123456789abcdefffffffff")); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "o_0123456789abcdefffffffff.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "o_0123456789abcdefffffffff.realm"); } SECTION("Null") { const bson::Bson partition; REQUIRE(partition.type() == bson::Bson::Type::Null); SyncConfig config(user, partition); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "null.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "null.realm"); } SECTION("Flexible sync") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config) == base_path / "flx_sync_default.realm"); + REQUIRE(app->path_for_realm(config) == base_path / "flx_sync_default.realm"); } SECTION("Custom filename for Flexible Sync") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom")) == base_path / "custom.realm"); } SECTION("Custom filename with type will still append .realm") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.foo")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.foo")) == base_path / "custom.foo.realm"); } SECTION("Custom filename for Flexible Sync including .realm") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.realm")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.realm")) == base_path / "custom.realm"); } SECTION("Custom filename for Flexible Sync with an existing path") { SyncConfig config(user, SyncConfig::FLXSyncEnabled{}); - std::string path = sync_manager->path_for_realm(config, util::make_optional("custom.realm")); + std::string path = app->path_for_realm(config, util::make_optional("custom.realm")); realm::test_util::TestPathGuard guard(path); realm::util::File existing_realm_file(path, File::mode_Write); existing_realm_file.write(std::string("test")); existing_realm_file.sync(); - REQUIRE(sync_manager->path_for_realm(config, util::make_optional("custom.realm")) == + REQUIRE(app->path_for_realm(config, util::make_optional("custom.realm")) == base_path / "custom.realm"); } @@ -180,527 +157,7 @@ TEST_CASE("sync_manager: `path_for_realm` API", "[sync][sync manager]") { } } -TEST_CASE("sync_manager: user state management", "[sync][sync manager]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - - const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); - const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); - const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); - - const std::string a_token_1 = ENCODE_FAKE_JWT("wibble"); - const std::string a_token_2 = ENCODE_FAKE_JWT("wobble"); - const std::string a_token_3 = ENCODE_FAKE_JWT("wubble"); - - const std::string identity_1 = "user-foo"; - const std::string identity_2 = "user-bar"; - const std::string identity_3 = "user-baz"; - - SECTION("should get all users that are created during run time") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 2); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - } - - SECTION("should be able to distinguish users based solely on user ID") { - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_2, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_3, r_token_1, a_token_1, dummy_device_id); - sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); // existing - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_1, a_token_1, dummy_device_id)); - } - - SECTION("should properly update state in response to users logging in and out") { - auto r_token_3a = ENCODE_FAKE_JWT("qwerty"); - auto a_token_3a = ENCODE_FAKE_JWT("ytrewq"); - - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - auto u3 = sync_manager->get_user(identity_3, r_token_3, a_token_3, dummy_device_id); - auto users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - // Log out users 1 and 3 - u1->log_out(); - u3->log_out(); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - // Log user 3 back in - u3 = sync_manager->get_user(identity_3, r_token_3a, a_token_3a, dummy_device_id); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); - // Log user 2 out - u2->log_out(); - users = sync_manager->all_users(); - REQUIRE(users.size() == 3); - CHECK(validate_user_in_vector(users, identity_3, r_token_3a, a_token_3a, dummy_device_id)); - } - - SECTION("should return current user that was created during run time") { - auto u_null = sync_manager->get_current_user(); - REQUIRE(u_null == nullptr); - - auto u1 = sync_manager->get_user(identity_1, r_token_1, a_token_1, dummy_device_id); - auto u_current = sync_manager->get_current_user(); - REQUIRE(u_current == u1); - - auto u2 = sync_manager->get_user(identity_2, r_token_2, a_token_2, dummy_device_id); - // The current user has switched to return the most recently used: "u2" - u_current = sync_manager->get_current_user(); - REQUIRE(u_current == u2); - } -} - -TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager]") { - TestSyncManager::Config config; - config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - TestSyncManager tsm(config); - config.base_path = tsm.base_file_path(); - config.should_teardown_test_directory = false; - auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); - const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); - const std::string r_token_3 = ENCODE_FAKE_JWT("baz_token"); - const std::string a_token_1 = ENCODE_FAKE_JWT("wibble"); - const std::string a_token_2 = ENCODE_FAKE_JWT("wobble"); - const std::string a_token_3 = ENCODE_FAKE_JWT("wubble"); - - SECTION("when users are persisted") { - const std::string identity_1 = "foo-1"; - const std::string identity_2 = "bar-1"; - const std::string identity_3 = "baz-1"; - // First, create a few users and add them to the metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1); - u1->set_access_token(a_token_1); - u1->set_refresh_token(r_token_1); - u1->set_device_id(dummy_device_id); - auto u2 = manager.get_or_make_user_metadata(identity_2); - u2->set_access_token(a_token_2); - u2->set_refresh_token(r_token_2); - u2->set_device_id(dummy_device_id); - auto u3 = manager.get_or_make_user_metadata(identity_3); - u3->set_access_token(a_token_3); - u3->set_refresh_token(r_token_3); - u3->set_device_id(dummy_device_id); - // The fourth user is an "invalid" user: no token, so shouldn't show up. - auto u_invalid = manager.get_or_make_user_metadata("invalid_user"); - REQUIRE(manager.all_unmarked_users().size() == 4); - - SECTION("they should be added to the active users list when metadata is enabled") { - TestSyncManager tsm2(config); - auto users = tsm2.sync_manager()->all_users(); - REQUIRE(users.size() == 3); - REQUIRE(validate_user_in_vector(users, identity_1, r_token_1, a_token_1, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_2, r_token_2, a_token_2, dummy_device_id)); - REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - } - - SECTION("they should not be added to the active users list when metadata is disabled") { - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm2(config); - auto users = tsm2.sync_manager()->all_users(); - REQUIRE(users.size() == 0); - } - } - - struct TestPath { - bson::Bson partition; - std::string expected_path; - bool pre_create = true; - }; - std::vector dirs_to_create; - std::vector paths_under_test; - - SECTION("when users are marked") { - const std::string identity_1 = "foo-2"; - const std::string identity_2 = "bar-2"; - const std::string identity_3 = "baz-2"; - - // Create the user metadata. - auto u1 = manager.get_or_make_user_metadata(identity_1); - auto u2 = manager.get_or_make_user_metadata(identity_2); - // Don't mark this user for deletion. - auto u3 = manager.get_or_make_user_metadata(identity_3); - - u1->set_legacy_identities({"legacy1"}); - u2->set_legacy_identities({"legacy2"}); - u3->set_legacy_identities({"legacy3"}); - - { - auto expected_u1_path = [&](const bson::Bson& partition) { - return ExpectedRealmPaths(tsm.base_file_path(), "app_id", u1->identity(), u1->legacy_identities(), - partition.to_string()); - }; - bson::Bson partition = "partition1"; - auto expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.current_preferred_path, false}); - - partition = "partition2"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.current_preferred_path, true}); - - partition = "partition3"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.fallback_hashed_path}); - - partition = "partition4"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.legacy_local_id_path}); - dirs_to_create.insert(dirs_to_create.end(), expected_paths.legacy_sync_directories_to_make.begin(), - expected_paths.legacy_sync_directories_to_make.end()); - - partition = "partition5"; - expected_paths = expected_u1_path(partition); - paths_under_test.push_back({partition, expected_paths.legacy_sync_path}); - dirs_to_create.insert(dirs_to_create.end(), expected_paths.legacy_sync_directories_to_make.begin(), - expected_paths.legacy_sync_directories_to_make.end()); - } - - std::vector paths; - { - auto sync_manager = tsm.sync_manager(); - - // Pre-populate the user directories. - auto user1 = sync_manager->get_user(u1->identity(), r_token_1, a_token_1, dummy_device_id); - auto user2 = sync_manager->get_user(u2->identity(), r_token_2, a_token_2, dummy_device_id); - auto user3 = sync_manager->get_user(u3->identity(), r_token_3, a_token_3, dummy_device_id); - for (auto& dir : dirs_to_create) { - try_make_dir(dir); - } - for (auto& test : paths_under_test) { - if (test.pre_create) { - create_dummy_realm(test.expected_path); - } - } - - paths = {sync_manager->path_for_realm(SyncConfig{user1, bson::Bson("123456789")}), - sync_manager->path_for_realm(SyncConfig{user1, bson::Bson("foo")}), - sync_manager->path_for_realm(SyncConfig{user2, bson::Bson("partition")}, {"123456789"}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("foo")}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("bar")}), - sync_manager->path_for_realm(SyncConfig{user3, bson::Bson("baz")})}; - - for (auto& test : paths_under_test) { - std::string actual = sync_manager->path_for_realm(SyncConfig{user1, test.partition}); - REQUIRE(actual == test.expected_path); - paths.push_back(actual); - } - - for (auto& path : paths) { - create_dummy_realm(path); - } - sync_manager->remove_user(u1->identity()); - sync_manager->remove_user(u2->identity()); - } - for (auto& path : paths) { - REQUIRE_REALM_EXISTS(path); - } - - config.should_teardown_test_directory = false; - SECTION("they should be cleaned up if metadata is enabled") { - TestSyncManager tsm(config); - auto users = tsm.sync_manager()->all_users(); - REQUIRE(users.size() == 1); - REQUIRE(validate_user_in_vector(users, identity_3, r_token_3, a_token_3, dummy_device_id)); - REQUIRE_REALM_DOES_NOT_EXIST(paths[0]); - REQUIRE_REALM_DOES_NOT_EXIST(paths[1]); - REQUIRE_REALM_DOES_NOT_EXIST(paths[2]); - REQUIRE_REALM_EXISTS(paths[3]); - REQUIRE_REALM_EXISTS(paths[4]); - REQUIRE_REALM_EXISTS(paths[5]); - // all the remaining user 1 realms should have been deleted - for (size_t i = 6; i < paths.size(); ++i) { - REQUIRE_REALM_DOES_NOT_EXIST(paths[i]); - } - } - SECTION("they should be left alone if metadata is disabled") { - config.should_teardown_test_directory = true; - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - auto users = tsm.sync_manager()->all_users(); - for (auto& path : paths) { - REQUIRE_REALM_EXISTS(path); - } - } - } -} - -TEST_CASE("sync_manager: file actions", "[sync][sync manager]") { - test_util::TestDirGuard guard(base_path.string()); - - using Action = SyncFileActionMetadata::Action; - - auto file_manager = SyncFileManager(base_path.string(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - TestSyncManager::Config config; - config.base_path = base_path.string(); - config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - config.should_teardown_test_directory = false; - - const std::string realm_url = "https://example.realm.com/~/1"; - const std::string partition = "partition_foo"; - const std::string uuid_1 = "uuid-foo-1"; - const std::string uuid_2 = "uuid-bar-1"; - const std::string uuid_3 = "uuid-baz-1"; - const std::string uuid_4 = "uuid-baz-2"; - - const std::vector legacy_identities; - - // Realm paths - const std::string realm_path_1 = file_manager.realm_file_path(uuid_1, legacy_identities, realm_url, partition); - const std::string realm_path_2 = file_manager.realm_file_path(uuid_2, legacy_identities, realm_url, partition); - const std::string realm_path_3 = file_manager.realm_file_path(uuid_3, legacy_identities, realm_url, partition); - const std::string realm_path_4 = file_manager.realm_file_path(uuid_4, legacy_identities, realm_url, partition); - - // On windows you can't delete a realm if the file is open elsewhere. -#ifdef _WIN32 - SECTION("Action::DeleteRealm - fails if locked") { - SharedRealm locked_realm; - create_dummy_realm(realm_path_1, &locked_realm); - - REQUIRE(locked_realm); - - TestSyncManager tsm(config); - manager.make_file_action_metadata(realm_path_1, Action::DeleteRealm); - - REQUIRE_FALSE(tsm.sync_manager()->immediately_run_file_actions(realm_path_1)); - } -#endif - - SECTION("Action::DeleteRealm") { - - // Create some file actions - manager.make_file_action_metadata(realm_path_1, Action::DeleteRealm); - manager.make_file_action_metadata(realm_path_2, Action::DeleteRealm); - manager.make_file_action_metadata(realm_path_3, Action::DeleteRealm); - - SECTION("should properly delete the Realm") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // All Realms should be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - } - - SECTION("should fail gracefully if the Realm is missing") { - // Don't actually create the Realm files - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - TestSyncManager tsm(config); - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - } - - SECTION("should do nothing if metadata is disabled") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - // All file actions should still be present. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 3); - // All Realms should still be present. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_EXISTS(realm_path_2); - REQUIRE_REALM_EXISTS(realm_path_3); - } - } - - SECTION("Action::BackUpThenDeleteRealm") { - const auto recovery_dir = file_manager.recovery_directory_path(); - // Create some file actions - const std::string recovery_1 = util::file_path_by_appending_component(recovery_dir, "recovery-1"); - const std::string recovery_2 = util::file_path_by_appending_component(recovery_dir, "recovery-2"); - const std::string recovery_3 = util::file_path_by_appending_component(recovery_dir, "recovery-3"); - manager.make_file_action_metadata(realm_path_1, Action::BackUpThenDeleteRealm, recovery_1); - manager.make_file_action_metadata(realm_path_2, Action::BackUpThenDeleteRealm, recovery_2); - manager.make_file_action_metadata(realm_path_3, Action::BackUpThenDeleteRealm, recovery_3); - - SECTION("should properly copy the Realm file and delete the Realm") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // All Realms should be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_1)); - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should copy the Realm to the recovery_directory_path") { - const std::string identity = "b241922032489d4836ecd0c82d0445f0"; - const auto realm_base_path = file_manager.realm_file_path(identity, {}, "realmtasks", partition); - std::string recovery_path = util::reserve_unique_file_name( - file_manager.recovery_directory_path(), util::create_timestamped_template("recovered_realm")); - create_dummy_realm(realm_base_path); - REQUIRE_REALM_EXISTS(realm_base_path); - REQUIRE(!File::exists(recovery_path)); - // Manually create a file action metadata entry to simulate a client reset. - manager.make_file_action_metadata(realm_base_path, Action::BackUpThenDeleteRealm, recovery_path); - auto pending_actions = manager.all_pending_actions(); - REQUIRE(pending_actions.size() == 4); - - // Simulate client launch. - TestSyncManager tsm(config); - - CHECK(pending_actions.size() == 0); - CHECK(File::exists(recovery_path)); - REQUIRE_REALM_DOES_NOT_EXIST(realm_base_path); - } - - SECTION("should fail gracefully if the Realm is missing") { - // Don't actually create the Realm files - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - TestSyncManager tsm(config); - // File actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 0); - // There should not be recovery files. - CHECK(!File::exists(recovery_1)); - CHECK(!File::exists(recovery_2)); - CHECK(!File::exists(recovery_3)); - } - - SECTION("should work properly when manually driven") { - REQUIRE(!File::exists(recovery_1)); - // Create a Realm file - create_dummy_realm(realm_path_4); - // Configure the system - TestSyncManager tsm(config); - REQUIRE(manager.all_pending_actions().size() == 0); - // Add a file action after the system is configured. - REQUIRE_REALM_EXISTS(realm_path_4); - REQUIRE(File::exists(file_manager.recovery_directory_path())); - manager.make_file_action_metadata(realm_path_4, Action::BackUpThenDeleteRealm, recovery_1); - REQUIRE(manager.all_pending_actions().size() == 1); - // Force the recovery. (In a real application, the user would have closed the files by now.) - REQUIRE(tsm.sync_manager()->immediately_run_file_actions(realm_path_4)); - // There should be recovery files. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_4); - CHECK(File::exists(recovery_1)); - REQUIRE(manager.all_pending_actions().size() == 0); - } - - SECTION("should fail gracefully if there is already a file at the destination") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - create_dummy_realm(recovery_1); - TestSyncManager tsm(config); - // Most file actions should be cleared. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 1); - // Realms should be deleted. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should change the action to delete if copy succeeds but delete fails") { - if (!chmod_supported(base_path.string())) { - return; - } - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - // remove secondary files so the action doesn't throw when it can't read these - File::try_remove(DB::get_core_file(realm_path_3, DB::CoreFileType::Note)); - File::try_remove(DB::get_core_file(realm_path_3, DB::CoreFileType::Log)); - util::try_remove_dir_recursive(DB::get_core_file(realm_path_3, DB::CoreFileType::Management)); - // remove write permissions of the parent directory so that removing realm3 will fail - std::string realm3_dir = File::parent_dir(realm_path_3); - realm3_dir = realm3_dir.empty() ? "." : realm3_dir; - int original_perms = get_permissions(realm3_dir); - realm::chmod(realm3_dir, original_perms & (~0b010000000)); // without owner_write - // run the actions - TestSyncManager tsm(config); - // restore write permissions to the directory - realm::chmod(realm3_dir, original_perms); - // Everything succeeded except deleting realm_path_3 - auto pending_actions = manager.all_pending_actions(); - REQUIRE(pending_actions.size() == 1); - // the realm3 action changed from BackUpThenDeleteRealm to DeleteRealm - CHECK(pending_actions.get(0).action() == Action::DeleteRealm); - CHECK(pending_actions.get(0).original_name() == realm_path_3); - CHECK(File::exists(recovery_3)); // the copy was successful - CHECK(File::exists(realm_path_3)); // the delete failed - // try again with proper permissions - REQUIRE(tsm.sync_manager()->immediately_run_file_actions(realm_path_3)); - REQUIRE(manager.all_pending_actions().size() == 0); - // Realms should all be deleted. - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_1); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_2); - REQUIRE_REALM_DOES_NOT_EXIST(realm_path_3); - // There should be recovery files. - CHECK(File::exists(recovery_2)); - CHECK(File::exists(recovery_3)); - } - - SECTION("should do nothing if metadata is disabled") { - // Create some Realms - create_dummy_realm(realm_path_1); - create_dummy_realm(realm_path_2); - create_dummy_realm(realm_path_3); - config.metadata_mode = SyncManager::MetadataMode::NoMetadata; - TestSyncManager tsm(config); - // All file actions should still be present. - auto pending_actions = manager.all_pending_actions(); - CHECK(pending_actions.size() == 3); - // All Realms should still be present. - REQUIRE_REALM_EXISTS(realm_path_1); - REQUIRE_REALM_EXISTS(realm_path_2); - REQUIRE_REALM_EXISTS(realm_path_3); - // There should not be recovery files. - CHECK(!File::exists(recovery_1)); - CHECK(!File::exists(recovery_2)); - CHECK(!File::exists(recovery_3)); - } - } -} - -TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { +TEST_CASE("SyncManager: set_session_multiplexing", "[sync][sync manager]") { TestSyncManager::Config tsm_config; tsm_config.start_sync_client = false; TestSyncManager tsm(tsm_config); @@ -708,8 +165,8 @@ TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { auto sync_manager = tsm.sync_manager(); sync_manager->set_session_multiplexing(sync_multiplexing_allowed); - auto user_1 = tsm.fake_user("user 1"); - auto user_2 = tsm.fake_user("user 2"); + auto user_1 = tsm.fake_user("user-name-1"); + auto user_2 = tsm.fake_user("user-name-2"); SyncTestFile file_1(user_1, "partition1", util::none); SyncTestFile file_2(user_1, "partition2", util::none); @@ -734,9 +191,9 @@ TEST_CASE("sync_manager: set_session_multiplexing", "[sync][sync manager]") { } } -TEST_CASE("sync_manager: has_existing_sessions", "[sync][sync manager][active sessions]") { - TestSyncManager init_sync_manager({}, {false}); - auto sync_manager = init_sync_manager.sync_manager(); +TEST_CASE("SyncManager: has_existing_sessions", "[sync][sync manager][active sessions]") { + TestSyncManager tsm({}, {false}); + auto sync_manager = tsm.sync_manager(); SECTION("no active sessions") { REQUIRE(!sync_manager->has_existing_sessions()); @@ -752,7 +209,7 @@ TEST_CASE("sync_manager: has_existing_sessions", "[sync][sync manager][active se std::atomic error_handler_invoked(false); Realm::Config config; - auto user = init_sync_manager.fake_user(); + auto user = tsm.fake_user("user-name"); auto create_session = [&](SyncSessionStopPolicy stop_policy) { std::shared_ptr session = sync_session( user, "/test-dying-state", diff --git a/test/object-store/sync/user.cpp b/test/object-store/sync/user.cpp deleted file mode 100644 index 57c0189d258..00000000000 --- a/test/object-store/sync/user.cpp +++ /dev/null @@ -1,303 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2016 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -#include -#include -#include - -#include -#include -#include - -#include -#include - -using namespace realm; -using namespace realm::util; -using File = realm::util::File; - -static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_user/"; -static const std::string dummy_device_id = "123400000000000000000000"; - -TEST_CASE("sync_user: SyncManager `get_user()` API", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly creates a new normal user") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user); - // The expected state for a newly created user: - REQUIRE(user->identity() == identity); - REQUIRE(user->refresh_token() == refresh_token); - REQUIRE(user->access_token() == access_token); - REQUIRE(user->state() == SyncUser::State::LoggedIn); - } - - SECTION("properly retrieves a previously created user, updating fields as necessary") { - const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); - const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first); - REQUIRE(first->identity() == identity); - REQUIRE(first->refresh_token() == refresh_token); - // Get the user again, but with a different token. - auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); - REQUIRE(second == first); - REQUIRE(second->identity() == identity); - REQUIRE(second->access_token() == second_access_token); - REQUIRE(second->refresh_token() == second_refresh_token); - } - - SECTION("properly resurrects a logged-out user") { - const std::string second_refresh_token = ENCODE_FAKE_JWT("0987654321-fake-refresh-token"); - const std::string second_access_token = ENCODE_FAKE_JWT("0987654321-fake-access-token"); - - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - first->log_out(); - REQUIRE(first->state() == SyncUser::State::LoggedOut); - // Get the user again, with a new token. - auto second = sync_manager->get_user(identity, second_refresh_token, second_access_token, dummy_device_id); - REQUIRE(second == first); - REQUIRE(second->identity() == identity); - REQUIRE(second->refresh_token() == second_refresh_token); - REQUIRE(second->state() == SyncUser::State::LoggedIn); - } -} - -TEST_CASE("sync_user: update state and tokens", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("fake-access-token-1"); - const std::string second_refresh_token = ENCODE_FAKE_JWT("fake-refresh-token-4"); - const std::string second_access_token = ENCODE_FAKE_JWT("fake-access-token-4"); - - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == refresh_token); - - user->log_in(second_access_token, second_refresh_token); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == second_refresh_token); - - user->log_out(); - REQUIRE(!user->is_logged_in()); - REQUIRE(user->refresh_token().empty()); - - user->log_in(access_token, refresh_token); - REQUIRE(user->is_logged_in()); - REQUIRE(user->refresh_token() == refresh_token); - - user->invalidate(); -} - -TEST_CASE("sync_user: SyncManager get_existing_logged_in_user() API", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly returns a null pointer when called for a non-existent user") { - std::shared_ptr user = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(!user); - } - - SECTION("can get logged-in user from notification") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - REQUIRE(first->device_id() == dummy_device_id); - bool notification_fired = false; - auto sub_token = first->subscribe([&](const SyncUser& user) { - auto current_user = sync_manager->get_current_user(); - REQUIRE(current_user->identity() == identity); - REQUIRE(current_user->identity() == user.identity()); - notification_fired = true; - }); - - auto second = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - second->unsubscribe(sub_token); - REQUIRE(notification_fired); - } - - SECTION("properly returns an existing logged-in user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedIn); - REQUIRE(first->device_id() == dummy_device_id); - // Get that user using the 'existing user' API. - auto second = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(second == first); - REQUIRE(second->refresh_token() == refresh_token); - } - - SECTION("properly returns a null pointer for a logged-out user") { - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - first->log_out(); - REQUIRE(first->identity() == identity); - REQUIRE(first->state() == SyncUser::State::LoggedOut); - // Get that user using the 'existing user' API. - auto second = sync_manager->get_existing_logged_in_user(identity); - REQUIRE(!second); - } -} - -TEST_CASE("sync_user: logout", "[sync][user]") { - TestSyncManager init_sync_manager; - auto sync_manager = init_sync_manager.sync_manager(); - const std::string identity = "sync_test_identity"; - const std::string refresh_token = ENCODE_FAKE_JWT("1234567890-fake-refresh-token"); - const std::string access_token = ENCODE_FAKE_JWT("1234567890-fake-access-token"); - - SECTION("properly changes the state of the user object") { - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - REQUIRE(user->state() == SyncUser::State::LoggedIn); - user->log_out(); - REQUIRE(user->state() == SyncUser::State::LoggedOut); - } -} - -TEST_CASE("sync_user: user persistence", "[sync][user]") { - TestSyncManager::Config tsm_config; - tsm_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - TestSyncManager tsm(tsm_config); - auto sync_manager = tsm.sync_manager(); - auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); - // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); - - SECTION("properly persists a user's information upon creation") { - const std::string identity = "test_identity_1"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile(identities, {}); - // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE((bool)metadata); - REQUIRE(metadata->is_valid()); - REQUIRE(metadata->access_token() == access_token); - REQUIRE(metadata->refresh_token() == refresh_token); - REQUIRE(metadata->device_id() == dummy_device_id); - REQUIRE(metadata->identities() == identities); - } - - SECTION("properly removes a user's access/refresh token upon log out") { - const std::string identity = "test_identity_1"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-1"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-1"); - const std::vector identities{{"12345", "test_case_provider"}}; - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile(identities, {}); - user->log_out(); - // Now try to pull the user out of the shadow manager directly. - auto metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE((bool)metadata); - REQUIRE(metadata->is_valid()); - REQUIRE(metadata->access_token() == ""); - REQUIRE(metadata->refresh_token() == ""); - REQUIRE(metadata->device_id() == dummy_device_id); - REQUIRE(metadata->identities() == identities); - REQUIRE(metadata->state() == SyncUser::State::LoggedOut); - REQUIRE(user->is_logged_in() == false); - } - - SECTION("properly persists a user's information when the user is updated") { - const std::string identity = "test_identity_2"; - const std::string refresh_token = ENCODE_FAKE_JWT("r_token-2a"); - const std::string access_token = ENCODE_FAKE_JWT("a_token-1a"); - // Create the user and validate it. - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - auto first_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(first_metadata->is_valid()); - REQUIRE(first_metadata->access_token() == access_token); - const std::string token_2 = ENCODE_FAKE_JWT("token-2b"); - // Update the user. - auto second = sync_manager->get_user(identity, refresh_token, token_2, dummy_device_id); - auto second_metadata = manager.get_or_make_user_metadata(identity, false); - REQUIRE(second_metadata->is_valid()); - REQUIRE(second_metadata->access_token() == token_2); - } - - SECTION("properly does not mark a user when the user is logged out and not anon") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Log out the user. - user->log_out(); - marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - } - - SECTION("properly removes a user when the user is logged out and is anon") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - user->update_user_profile({{"id", app::IdentityProviderAnonymous}}, {}); - auto marked_users = manager.all_users_marked_for_removal(); - REQUIRE(marked_users.size() == 0); - // Log out the user. - user->log_out(); - REQUIRE(sync_manager->all_users().size() == 0); - } - - SECTION("properly revives a logged-out user when it's requested again") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-4a"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-4a"); - // Create the user and log it out. - auto first = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - first->log_out(); - REQUIRE(sync_manager->all_users().size() == 1); - REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedOut); - // Log the user back in. - const std::string r_token_2 = ENCODE_FAKE_JWT("r-token-4b"); - const std::string a_token_2 = ENCODE_FAKE_JWT("atoken-4b"); - auto second = sync_manager->get_user(identity, r_token_2, a_token_2, dummy_device_id); - REQUIRE(sync_manager->all_users().size() == 1); - REQUIRE(sync_manager->all_users()[0]->state() == SyncUser::State::LoggedIn); - } - - SECTION("properly deletes a user") { - const std::string identity = "test_identity_3"; - const std::string refresh_token = ENCODE_FAKE_JWT("r-token-3"); - const std::string access_token = ENCODE_FAKE_JWT("a-token-3"); - // Create the user and validate it. - auto user = sync_manager->get_user(identity, refresh_token, access_token, dummy_device_id); - sync_manager->set_current_user(identity); - REQUIRE(sync_manager->get_current_user() == user); - REQUIRE(sync_manager->all_users().size() == 1); - sync_manager->delete_user(user->identity()); - REQUIRE(sync_manager->all_users().size() == 0); - REQUIRE(sync_manager->get_current_user() == nullptr); - } -} diff --git a/test/object-store/util/sync/baas_admin_api.hpp b/test/object-store/util/sync/baas_admin_api.hpp index ce85d8ecc22..35aa8402de9 100644 --- a/test/object-store/util/sync/baas_admin_api.hpp +++ b/test/object-store/util/sync/baas_admin_api.hpp @@ -291,7 +291,7 @@ AppSession get_runtime_app_session(); std::string get_mongodb_server(); template -inline app::App::Config get_config(Factory factory, const AppSession& app_session) +inline app::AppConfig get_config(Factory factory, const AppSession& app_session) { return {app_session.client_app_id, factory, diff --git a/test/object-store/util/sync/sync_test_utils.cpp b/test/object-store/util/sync/sync_test_utils.cpp index a4a2d88531e..4027010f2f1 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(); @@ -240,6 +219,8 @@ std::string get_admin_url() } #endif // REALM_MONGODB_ENDPOINT +#endif // REALM_ENABLE_AUTH_TESTS + AutoVerifiedEmailCredentials::AutoVerifiedEmailCredentials() { // emails with this prefix will pass through the baas app due to the register function @@ -314,8 +295,6 @@ void async_open_realm(const Realm::Config& config, task->cancel(); // don't run the above notifier again on this session finish(std::move(tsr), err); } - -#endif // REALM_ENABLE_AUTH_TESTS #endif // REALM_ENABLE_SYNC class TestHelper { @@ -459,7 +438,7 @@ struct FakeLocalClientReset : public TestClientReset { #if REALM_ENABLE_AUTH_TESTS -void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, +void wait_for_object_to_persist_to_atlas(std::shared_ptr user, const AppSession& app_session, const std::string& schema_name, const bson::BsonDocument& filter_bson) { // While at this point the object has been sync'd successfully, we must also @@ -490,7 +469,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 7474e8c47b2..002b1192f0b 100644 --- a/test/object-store/util/sync/sync_test_utils.hpp +++ b/test/object-store/util/sync/sync_test_utils.hpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include @@ -46,9 +46,6 @@ namespace realm { -bool results_contains_user(SyncUserMetadataResults& results, const std::string& identity); -bool results_contains_original_name(SyncFileActionMetadataResults& results, const std::string& original_name); - void timed_wait_for(util::FunctionRef condition, std::chrono::milliseconds max_ms = std::chrono::milliseconds(5000)); @@ -137,6 +134,14 @@ std::ostream& operator<<(std::ostream& os, util::Optional error); void subscribe_to_all_and_bootstrap(Realm& realm); +struct AutoVerifiedEmailCredentials : app::AppCredentials { + AutoVerifiedEmailCredentials(); + std::string email; + std::string password; +}; + +AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); + #if REALM_ENABLE_AUTH_TESTS void wait_for_sessions_to_close(const TestAppSession& test_app_session); @@ -144,24 +149,14 @@ void wait_for_sessions_to_close(const TestAppSession& test_app_session); #ifdef REALM_MONGODB_ENDPOINT std::string get_base_url(); std::string get_admin_url(); - #endif - -struct AutoVerifiedEmailCredentials : app::AppCredentials { - AutoVerifiedEmailCredentials(); - std::string email; - std::string password; -}; - -AutoVerifiedEmailCredentials create_user_and_log_in(app::SharedApp app); +#endif // REALM_ENABLE_AUTH_TESTS void wait_for_advance(Realm& realm); void async_open_realm(const Realm::Config& config, util::UniqueFunction finish); -#endif // REALM_ENABLE_AUTH_TESTS - #endif // REALM_ENABLE_SYNC namespace reset_utils { @@ -220,10 +215,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 2b984f38157..f9fbfb57d2d 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 @@ -132,11 +134,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)) { @@ -191,8 +188,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)) { } @@ -272,7 +269,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(); @@ -304,7 +301,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) @@ -352,19 +349,18 @@ TestAppSession::TestAppSession(AppSession session, auto app_config = get_config(m_transport, *m_app_session); util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); set_app_config_defaults(app_config, m_transport); + app_config.base_file_path = m_base_file_path; + app_config.metadata_mode = realm::app::AppConfig::MetadataMode::NoEncryption; util::try_make_dir(m_base_file_path); - SyncClientConfig sc_config; - sc_config.base_file_path = m_base_file_path; - sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoEncryption; - sc_config.reconnect_mode = reconnect_mode; - sc_config.socket_provider = custom_socket_provider; + app_config.sync_client_config.reconnect_mode = reconnect_mode; + app_config.sync_client_config.socket_provider = custom_socket_provider; // With multiplexing enabled, the linger time controls how long a // connection is kept open for reuse. In tests, we want to shut // down sync clients immediately. - sc_config.timeouts.connection_linger_time = 0; + app_config.sync_client_config.timeouts.connection_linger_time = 0; - m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config, sc_config); + m_app = app::App::get_app(app::App::CacheMode::Disabled, app_config); // initialize sync client m_app->sync_manager()->get_sync_client(); @@ -388,7 +384,7 @@ TestAppSession::~TestAppSession() } } -std::vector TestAppSession::get_documents(SyncUser& user, const std::string& object_type, +std::vector TestAppSession::get_documents(app::User& user, const std::string& object_type, size_t expected_count) const { app::MongoClient remote_client = user.mongo_client("BackingDB"); @@ -434,17 +430,15 @@ std::vector TestAppSession::get_documents(SyncUser& user, co // MARK: - TestSyncManager 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); util::Logger::set_default_level_threshold(config.log_level); - SyncClientConfig sc_config; - sc_config.base_file_path = m_base_file_path; - sc_config.metadata_mode = config.metadata_mode; - m_sync_manager = SyncManager::create(nullptr, m_sync_server.base_url() + "/realm-sync", sc_config, "app_id"); + m_sync_manager->set_sync_route(m_sync_server.base_url() + "/realm-sync"); if (config.start_sync_client) { m_sync_manager->get_sync_client(); } @@ -466,9 +460,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) @@ -481,6 +478,11 @@ OfflineAppSession::OfflineAppSession(OfflineAppSession::Config config) , m_delete_storage(config.delete_storage) { REALM_ASSERT(m_transport); + app::AppConfig app_config; + set_app_config_defaults(app_config, m_transport); + + util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); + if (config.storage_path) { m_base_file_path = *config.storage_path; util::try_make_dir(m_base_file_path); @@ -489,23 +491,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; - - util::Logger::set_default_level_threshold(realm::util::Logger::Level::TEST_LOGGING_LEVEL); - - 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() @@ -522,9 +517,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 af7aa38e449..8d5b34b5113 100644 --- a/test/object-store/util/test_file.hpp +++ b/test/object-store/util/test_file.hpp @@ -160,6 +160,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 @@ -179,7 +248,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); }; @@ -188,7 +257,6 @@ class TestSyncManager { struct Config { Config() {} std::string base_path; - realm::SyncManager::MetadataMode metadata_mode = realm::SyncManager::MetadataMode::NoMetadata; bool should_teardown_test_directory = true; realm::util::Logger::Level log_level = realm::util::Logger::Level::TEST_LOGGING_LEVEL; bool start_sync_client = true; @@ -210,13 +278,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 { @@ -226,7 +294,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; @@ -238,7 +306,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(); @@ -286,7 +354,7 @@ class TestAppSession { return m_app->sync_manager(); } - std::vector get_documents(realm::SyncUser& user, const std::string& object_type, + std::vector get_documents(realm::app::User& user, const std::string& object_type, size_t expected_count) const; private: @@ -301,7 +369,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