//////////////////////////////////////////////////////////////////////////// // // 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. // //////////////////////////////////////////////////////////////////////////// #import "RLMSyncUser_Private.hpp" #import "RLMJSONModels.h" #import "RLMNetworkClient.h" #import "RLMRealmConfiguration+Sync.h" #import "RLMRealmConfiguration_Private.hpp" #import "RLMRealmUtil.hpp" #import "RLMResults_Private.hpp" #import "RLMSyncConfiguration.h" #import "RLMSyncConfiguration_Private.hpp" #import "RLMSyncManager_Private.h" #import "RLMSyncPermissionResults.h" #import "RLMSyncPermission_Private.hpp" #import "RLMSyncSessionRefreshHandle.hpp" #import "RLMSyncSession_Private.hpp" #import "RLMSyncUtil_Private.hpp" #import "RLMUtil.hpp" #import "sync/sync_manager.hpp" #import "sync/sync_session.hpp" #import "sync/sync_user.hpp" using namespace realm; using ConfigMaker = std::function, std::string)>; namespace { std::function RLMWrapPermissionResultsCallback(RLMPermissionResultsBlock callback) { return [callback](Results results, std::exception_ptr ptr) { if (ptr) { NSError *error = translateSyncExceptionPtrToError(std::move(ptr), RLMPermissionActionTypeGet); REALM_ASSERT(error); callback(nil, error); } else { // Finished successfully callback([[RLMSyncPermissionResults alloc] initWithResults:std::move(results)], nil); } }; } NSString *tildeSubstitutedPathForRealmURL(NSURL *url, NSString *identity) { return [[url path] stringByReplacingOccurrencesOfString:@"~" withString:identity]; } } void CocoaSyncUserContext::register_refresh_handle(const std::string& path, RLMSyncSessionRefreshHandle *handle) { REALM_ASSERT(handle); std::lock_guard lock(m_mutex); auto it = m_refresh_handles.find(path); if (it != m_refresh_handles.end()) { [it->second invalidate]; m_refresh_handles.erase(it); } m_refresh_handles.insert({path, handle}); } void CocoaSyncUserContext::unregister_refresh_handle(const std::string& path) { std::lock_guard lock(m_mutex); m_refresh_handles.erase(path); } void CocoaSyncUserContext::invalidate_all_handles() { std::lock_guard lock(m_mutex); for (auto& it : m_refresh_handles) { [it.second invalidate]; } m_refresh_handles.clear(); } RLMUserErrorReportingBlock CocoaSyncUserContext::error_handler() const { std::lock_guard lock(m_error_handler_mutex); return m_error_handler; } void CocoaSyncUserContext::set_error_handler(RLMUserErrorReportingBlock block) { std::lock_guard lock(m_error_handler_mutex); m_error_handler = block; } PermissionChangeCallback RLMWrapPermissionStatusCallback(RLMPermissionStatusBlock callback) { return [callback](std::exception_ptr ptr) { if (ptr) { NSError *error = translateSyncExceptionPtrToError(std::move(ptr), RLMPermissionActionTypeChange); REALM_ASSERT(error); callback(error); } else { // Finished successfully callback(nil); } }; } @interface RLMSyncUserInfo () @property (nonatomic, readwrite) NSArray *accounts; @property (nonatomic, readwrite) NSDictionary *metadata; @property (nonatomic, readwrite) NSString *identity; @property (nonatomic, readwrite) BOOL isAdmin; + (instancetype)syncUserInfoWithModel:(RLMUserResponseModel *)model; @end @interface RLMSyncUser () { std::shared_ptr _user; // FIXME: remove this when the object store ConfigMaker goes away std::unique_ptr _configMaker; } - (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; @end @implementation RLMSyncUser #pragma mark - static API + (NSDictionary *)allUsers { NSArray *allUsers = [[RLMSyncManager sharedManager] _allUsers]; return [NSDictionary dictionaryWithObjects:allUsers forKeys:[allUsers valueForKey:@"identity"]]; } + (RLMSyncUser *)currentUser { NSArray *allUsers = [[RLMSyncManager sharedManager] _allUsers]; if (allUsers.count > 1) { @throw RLMException(@"+currentUser cannot be called if more that one valid, logged-in user exists."); } return allUsers.firstObject; } #pragma mark - API - (instancetype)initPrivate { if (self = [super init]) { _configMaker = std::make_unique([](std::shared_ptr user, std::string url) { NSURL *objCUrl = [NSURL URLWithString:@(url.c_str())]; RLMSyncUser *objCUser = [[RLMSyncUser alloc] initWithSyncUser:std::move(user)]; return [objCUser configurationWithURL:objCUrl fullSynchronization:true].config; }); return self; } return nil; } - (instancetype)initWithSyncUser:(std::shared_ptr)user { if (self = [self initPrivate]) { _user = user; return self; } return nil; } - (BOOL)isEqual:(id)object { if (![object isKindOfClass:[RLMSyncUser class]]) { return NO; } return _user == ((RLMSyncUser *)object)->_user; } + (void)logInWithCredentials:(RLMSyncCredentials *)credential authServerURL:(NSURL *)authServerURL onCompletion:(RLMUserCompletionBlock)completion { [self logInWithCredentials:credential authServerURL:authServerURL timeout:30 callbackQueue:dispatch_get_main_queue() onCompletion:completion]; } + (void)logInWithCredentials:(RLMSyncCredentials *)credential authServerURL:(NSURL *)authServerURL timeout:(NSTimeInterval)timeout callbackQueue:(dispatch_queue_t)callbackQueue onCompletion:(RLMUserCompletionBlock)completion { RLMSyncUser *user = [[RLMSyncUser alloc] initPrivate]; [RLMSyncUser _performLogInForUser:user credentials:credential authServerURL:authServerURL timeout:timeout callbackQueue:callbackQueue completionBlock:completion]; } - (RLMRealmConfiguration *)configuration { return [self configurationWithURL:nil fullSynchronization:NO enableSSLValidation:YES urlPrefix:nil]; } - (RLMRealmConfiguration *)configurationWithURL:(NSURL *)url { return [self configurationWithURL:url fullSynchronization:NO enableSSLValidation:YES urlPrefix:nil]; } - (RLMRealmConfiguration *)configurationWithURL:(NSURL *)url fullSynchronization:(bool)fullSynchronization { return [self configurationWithURL:url fullSynchronization:fullSynchronization enableSSLValidation:YES urlPrefix:nil]; } - (RLMRealmConfiguration *)configurationWithURL:(NSURL *)url fullSynchronization:(bool)fullSynchronization enableSSLValidation:(bool)enableSSLValidation urlPrefix:(NSString * _Nullable)urlPrefix { auto syncConfig = [[RLMSyncConfiguration alloc] initWithUser:self realmURL:url ?: self.defaultRealmURL customFileURL:nil isPartial:!fullSynchronization stopPolicy:RLMSyncStopPolicyAfterChangesUploaded errorHandler:nullptr]; syncConfig.urlPrefix = urlPrefix; syncConfig.enableSSLValidation = enableSSLValidation; syncConfig.pinnedCertificateURL = RLMSyncManager.sharedManager.pinnedCertificatePaths[syncConfig.realmURL.host]; RLMRealmConfiguration *config = [[RLMRealmConfiguration alloc] init]; config.syncConfiguration = syncConfig; return config; } - (void)logOut { if (!_user) { return; } _user->log_out(); context_for(_user).invalidate_all_handles(); } - (RLMUserErrorReportingBlock)errorHandler { if (!_user) { return nil; } return context_for(_user).error_handler(); } - (void)setErrorHandler:(RLMUserErrorReportingBlock)errorHandler { if (!_user) { return; } context_for(_user).set_error_handler([errorHandler copy]); } - (nullable RLMSyncSession *)sessionForURL:(NSURL *)url { if (!_user) { return nil; } auto path = SyncManager::shared().path_for_realm(*_user, [url.absoluteString UTF8String]); if (auto session = _user->session_for_on_disk_path(path)) { return [[RLMSyncSession alloc] initWithSyncSession:session]; } return nil; } - (NSArray *)allSessions { if (!_user) { return @[]; } NSMutableArray *buffer = [NSMutableArray array]; auto sessions = _user->all_sessions(); for (auto session : sessions) { [buffer addObject:[[RLMSyncSession alloc] initWithSyncSession:std::move(session)]]; } return [buffer copy]; } - (NSString *)identity { if (!_user) { return nil; } return @(_user->identity().c_str()); } - (RLMSyncUserState)state { if (!_user) { return RLMSyncUserStateError; } switch (_user->state()) { case SyncUser::State::Active: return RLMSyncUserStateActive; case SyncUser::State::LoggedOut: return RLMSyncUserStateLoggedOut; case SyncUser::State::Error: return RLMSyncUserStateError; } } - (NSURL *)authenticationServer { if (!_user || _user->token_type() == SyncUser::TokenType::Admin) { return nil; } return [NSURL URLWithString:@(_user->server_url().c_str())]; } - (BOOL)isAdmin { if (!_user) { return NO; } return _user->is_admin(); } #pragma mark - Passwords - (void)changePassword:(NSString *)newPassword completion:(RLMPasswordChangeStatusBlock)completion { [self changePassword:newPassword forUserID:self.identity completion:completion]; } - (void)changePassword:(NSString *)newPassword forUserID:(NSString *)userID completion:(RLMPasswordChangeStatusBlock)completion { if (self.state != RLMSyncUserStateActive) { completion([NSError errorWithDomain:RLMSyncErrorDomain code:RLMSyncErrorClientSessionError userInfo:nil]); return; } [RLMSyncChangePasswordEndpoint sendRequestToServer:self.authenticationServer JSON:@{kRLMSyncTokenKey: self.refreshToken, kRLMSyncUserIDKey: userID, kRLMSyncDataKey: @{kRLMSyncNewPasswordKey: newPassword}} options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:completion]; } + (void)requestPasswordResetForAuthServer:(NSURL *)serverURL userEmail:(NSString *)email completion:(RLMPasswordChangeStatusBlock)completion { [RLMSyncUpdateAccountEndpoint sendRequestToServer:serverURL JSON:@{@"provider_id": email, @"data": @{@"action": @"reset_password"}} options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:completion]; } + (void)completePasswordResetForAuthServer:(NSURL *)serverURL token:(NSString *)token password:(NSString *)newPassword completion:(RLMPasswordChangeStatusBlock)completion { [RLMSyncUpdateAccountEndpoint sendRequestToServer:serverURL JSON:@{@"data": @{@"action": @"complete_reset", @"token": token, @"new_password": newPassword}} options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:completion]; } + (void)requestEmailConfirmationForAuthServer:(NSURL *)serverURL userEmail:(NSString *)email completion:(RLMPasswordChangeStatusBlock)completion { [RLMSyncUpdateAccountEndpoint sendRequestToServer:serverURL JSON:@{@"provider_id": email, @"data": @{@"action": @"request_email_confirmation"}} options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:completion]; } + (void)confirmEmailForAuthServer:(NSURL *)serverURL token:(NSString *)token completion:(RLMPasswordChangeStatusBlock)completion { [RLMSyncUpdateAccountEndpoint sendRequestToServer:serverURL JSON:@{@"data": @{@"action": @"confirm_email", @"token": token}} options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:completion]; } #pragma mark - Administrator API - (void)retrieveInfoForUser:(NSString *)providerUserIdentity identityProvider:(RLMIdentityProvider)provider completion:(RLMRetrieveUserBlock)completion { [RLMNetworkClient sendRequestToEndpoint:[RLMSyncGetUserInfoEndpoint endpoint] server:self.authenticationServer JSON:@{ kRLMSyncProviderKey: provider, kRLMSyncProviderIDKey: providerUserIdentity, kRLMSyncTokenKey: self.refreshToken } timeout:60 options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:^(NSError *error, NSDictionary *json) { if (error) { completion(nil, error); return; } RLMUserResponseModel *model = [[RLMUserResponseModel alloc] initWithDictionary:json]; if (!model) { completion(nil, make_auth_error_bad_response(json)); return; } completion([RLMSyncUserInfo syncUserInfoWithModel:model], nil); }]; } #pragma mark - Permissions API static void verifyInRunLoop() { if (!RLMIsInRunLoop()) { @throw RLMException(@"Can only access or modify permissions from a thread which has a run loop (by default, only the main thread)."); } } - (void)retrievePermissionsWithCallback:(RLMPermissionResultsBlock)callback { verifyInRunLoop(); if (!_user || _user->state() == SyncUser::State::Error) { callback(nullptr, make_permission_error_get(@"Permissions cannot be retrieved using an invalid user.")); return; } Permissions::get_permissions(_user, RLMWrapPermissionResultsCallback(callback), *_configMaker); } - (void)applyPermission:(RLMSyncPermission *)permission callback:(RLMPermissionStatusBlock)callback { verifyInRunLoop(); if (!_user || _user->state() == SyncUser::State::Error) { callback(make_permission_error_change(@"Permissions cannot be applied using an invalid user.")); return; } Permissions::set_permission(_user, [permission rawPermission], RLMWrapPermissionStatusCallback(callback), *_configMaker); } - (void)revokePermission:(RLMSyncPermission *)permission callback:(RLMPermissionStatusBlock)callback { verifyInRunLoop(); if (!_user || _user->state() == SyncUser::State::Error) { callback(make_permission_error_change(@"Permissions cannot be revoked using an invalid user.")); return; } Permissions::delete_permission(_user, [permission rawPermission], RLMWrapPermissionStatusCallback(callback), *_configMaker); } - (void)createOfferForRealmAtURL:(NSURL *)url accessLevel:(RLMSyncAccessLevel)accessLevel expiration:(NSDate *)expirationDate callback:(RLMPermissionOfferStatusBlock)callback { verifyInRunLoop(); if (!_user || _user->state() == SyncUser::State::Error) { callback(nil, make_permission_error_change(@"A permission offer cannot be created using an invalid user.")); return; } auto cb = [callback](util::Optional token, std::exception_ptr ptr) { if (ptr) { NSError *error = translateSyncExceptionPtrToError(std::move(ptr), RLMPermissionActionTypeOffer); REALM_ASSERT_DEBUG(error); callback(nil, error); } else { REALM_ASSERT_DEBUG(token); callback(@(token->c_str()), nil); } }; auto offer = PermissionOffer{ [tildeSubstitutedPathForRealmURL(url, self.identity) UTF8String], accessLevelForObjCAccessLevel(accessLevel), RLMTimestampForNSDate(expirationDate), }; Permissions::make_offer(_user, std::move(offer), std::move(cb), *_configMaker); } - (void)acceptOfferForToken:(NSString *)token callback:(RLMPermissionOfferResponseStatusBlock)callback { verifyInRunLoop(); if (!_user || _user->state() == SyncUser::State::Error) { callback(nil, make_permission_error_change(@"A permission offer cannot be accepted by an invalid user.")); return; } NSURLComponents *baseURL = [NSURLComponents componentsWithURL:self.authenticationServer resolvingAgainstBaseURL:YES]; if ([baseURL.scheme isEqualToString:@"http"]) { baseURL.scheme = @"realm"; } else if ([baseURL.scheme isEqualToString:@"https"]) { baseURL.scheme = @"realms"; } auto cb = [baseURL, callback](util::Optional raw_path, std::exception_ptr ptr) { if (ptr) { NSError *error = translateSyncExceptionPtrToError(std::move(ptr), RLMPermissionActionTypeAcceptOffer); REALM_ASSERT_DEBUG(error); callback(nil, error); } else { // Note that ROS currently vends the path to the Realm, so we need to construct the full URL ourselves. REALM_ASSERT_DEBUG(raw_path); baseURL.path = @(raw_path->c_str()); callback([baseURL URL], nil); } }; Permissions::accept_offer(_user, [token UTF8String], std::move(cb), *_configMaker); } #pragma mark - Private API - (NSURL *)defaultRealmURL { NSURLComponents *components = [NSURLComponents componentsWithURL:self.authenticationServer resolvingAgainstBaseURL:YES]; if ([components.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame) components.scheme = @"realm"; else if ([components.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) components.scheme = @"realms"; else @throw RLMException(@"The provided user's authentication server URL (%@) was not valid.", self.authenticationServer); components.path = @"/default"; return components.URL; } + (void)_setUpBindingContextFactory { SyncUser::set_binding_context_factory([] { return std::make_shared(); }); } - (NSString *)refreshToken { if (!_user) { return nil; } return @(_user->refresh_token().c_str()); } - (std::shared_ptr)_syncUser { return _user; } + (void)_performLogInForUser:(RLMSyncUser *)user credentials:(RLMSyncCredentials *)credentials authServerURL:(NSURL *)authServerURL timeout:(NSTimeInterval)timeout callbackQueue:(dispatch_queue_t)callbackQueue completionBlock:(RLMUserCompletionBlock)completion { // Special credential login should be treated differently. if (credentials.provider == RLMIdentityProviderAccessToken) { [self _performLoginForDirectAccessTokenCredentials:credentials user:user authServerURL:authServerURL completionBlock:completion]; return; } if (!authServerURL) { @throw RLMException(@"A user cannot be logged in without specifying an authentication server URL."); } // Prepare login network request NSMutableDictionary *json = [@{ kRLMSyncProviderKey: credentials.provider, kRLMSyncDataKey: credentials.token, kRLMSyncAppIDKey: [RLMSyncManager sharedManager].appID, } mutableCopy]; NSMutableDictionary *info = [(credentials.userInfo ?: @{}) mutableCopy]; if ([info count] > 0) { // Munge user info into the JSON request. json[@"user_info"] = info; } RLMSyncCompletionBlock handler = ^(NSError *error, NSDictionary *json) { if (json && !error) { RLMAuthResponseModel *model = [[RLMAuthResponseModel alloc] initWithDictionary:json requireAccessToken:NO requireRefreshToken:YES]; if (!model) { // Malformed JSON NSError *badResponseError = make_auth_error_bad_response(json); dispatch_async(callbackQueue, ^{ completion(nil, badResponseError); }); return; } else { std::string server_url = authServerURL.absoluteString.UTF8String; SyncUserIdentifier identity{[model.refreshToken.tokenData.identity UTF8String], std::move(server_url)}; auto sync_user = SyncManager::shared().get_user(identity , [model.refreshToken.token UTF8String]); if (!sync_user) { NSError *authError = make_auth_error_client_issue(); dispatch_async(callbackQueue, ^{ completion(nil, authError); }); return; } sync_user->set_is_admin(model.refreshToken.tokenData.isAdmin); user->_user = sync_user; dispatch_async(callbackQueue, ^{ completion(user, nil); }); } } else { // Something else went wrong dispatch_async(callbackQueue, ^{ completion(nil, error); }); } }; [RLMNetworkClient sendRequestToEndpoint:[RLMSyncAuthEndpoint endpoint] server:authServerURL JSON:json timeout:timeout options:[[RLMSyncManager sharedManager] networkRequestOptions] completion:^(NSError *error, NSDictionary *dictionary) { dispatch_async(callbackQueue, ^{ handler(error, dictionary); }); }]; } + (void)_performLoginForDirectAccessTokenCredentials:(RLMSyncCredentials *)credentials user:(RLMSyncUser *)user authServerURL:(NSURL *)serverURL completionBlock:(nonnull RLMUserCompletionBlock)completion { NSString *identity = credentials.userInfo[kRLMSyncIdentityKey]; std::shared_ptr sync_user; if (serverURL) { NSString *scheme = serverURL.scheme; if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) { @throw RLMException(@"The Realm Object Server authentication URL provided for this user, \"%@\", " @" is invalid. It must begin with http:// or https://.", serverURL); } // Retrieve the user based on the auth server URL. util::Optional identity_string; if (identity) { identity_string = std::string(identity.UTF8String); } sync_user = SyncManager::shared().get_admin_token_user([serverURL absoluteString].UTF8String, credentials.token.UTF8String, std::move(identity_string)); } else { // Retrieve the user based on the identity. if (!identity) { @throw RLMException(@"A direct access credential must specify either an identity, a server URL, or both."); } sync_user = SyncManager::shared().get_admin_token_user_from_identity(identity.UTF8String, none, credentials.token.UTF8String); } if (!sync_user) { completion(nil, make_auth_error_client_issue()); return; } user->_user = sync_user; completion(user, nil); } @end #pragma mark - RLMSyncUserInfo @implementation RLMSyncUserInfo - (instancetype)initPrivate { return [super init]; } + (instancetype)syncUserInfoWithModel:(RLMUserResponseModel *)model { RLMSyncUserInfo *info = [[RLMSyncUserInfo alloc] initPrivate]; info.accounts = model.accounts; info.metadata = model.metadata; info.isAdmin = model.isAdmin; info.identity = model.identity; return info; } @end