diff --git a/Realm/Realm/Handles/SyncUserHandle.cs b/Realm/Realm/Handles/SyncUserHandle.cs index 46a38dd9e7..4e959d7601 100644 --- a/Realm/Realm/Handles/SyncUserHandle.cs +++ b/Realm/Realm/Handles/SyncUserHandle.cs @@ -88,7 +88,10 @@ public static extern void call_function(SyncUserHandle handle, AppHandle app, IntPtr tcs_ptr, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncuser_link_credentials", CallingConvention = CallingConvention.Cdecl)] - public static extern void link_credentials(SyncUserHandle hadnle, AppHandle app, Native.Credentials credentials, IntPtr tcs_ptr, out NativeException ex); + public static extern void link_credentials(SyncUserHandle handle, AppHandle app, Native.Credentials credentials, IntPtr tcs_ptr, out NativeException ex); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncuser_get_serialized_identities", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_identities(SyncUserHandle handle, IntPtr buffer, IntPtr bufsize, out NativeException ex); #region Api Keys @@ -253,6 +256,15 @@ public void LinkCredentials(AppHandle app, Native.Credentials credentials, TaskC ex.ThrowIfNecessary(tcsHandle); } + public string GetIdentities() + { + return MarshalHelpers.GetString((IntPtr buffer, IntPtr length, out bool isNull, out NativeException ex) => + { + isNull = false; + return NativeMethods.get_identities(this, buffer, length, out ex); + }); + } + #region Api Keys public void CreateApiKey(AppHandle app, string name, TaskCompletionSource tcs) diff --git a/Realm/Realm/Sync/User.cs b/Realm/Realm/Sync/User.cs index 166d9e021d..455d9a4004 100644 --- a/Realm/Realm/Sync/User.cs +++ b/Realm/Realm/Sync/User.cs @@ -23,6 +23,7 @@ using System.Linq; using System.Threading.Tasks; using MongoDB.Bson; +using MongoDB.Bson.Serialization; using Realms.Helpers; using Realms.Native; using Realms.Sync.Exceptions; @@ -115,6 +116,19 @@ public BsonDocument CustomData } } + /// + /// Gets a collection of all identities associated with this user. + /// + /// The user's identities across different s. + public UserIdentity[] Identities + { + get + { + var serialized = Handle.GetIdentities(); + return BsonSerializer.Deserialize(serialized); + } + } + /// /// Gets a instance that exposes functionality about managing user API keys. /// diff --git a/Realm/Realm/Sync/UserIdentity.cs b/Realm/Realm/Sync/UserIdentity.cs new file mode 100644 index 0000000000..5e6b82ffdf --- /dev/null +++ b/Realm/Realm/Sync/UserIdentity.cs @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2020 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. +// +//////////////////////////////////////////////////////////////////////////// + +using MongoDB.Bson.Serialization.Attributes; + +namespace Realms.Sync +{ + /// + /// A class containing information about an identity associated with a user. + /// + [BsonNoId] + public class UserIdentity + { + /// + /// Gets the unique identifier for this identity. + /// + /// The identity's Id. + public string Id { get; private set; } + + /// + /// Gets the auth provider defining this identity. + /// + /// The identity's auth provider. + public Credentials.AuthProvider Provider { get; private set; } + + /// + public override bool Equals(object obj) => (obj is UserIdentity id) && id.Id == Id && id.Provider == Provider; + + /// + /// Gets the hash code. + /// + /// The hash code. + public override int GetHashCode() + { + var hashCode = -1285871140; + hashCode = (hashCode * -1521134295) + (Id?.GetHashCode() ?? 0); + hashCode = (hashCode * -1521134295) + Provider.GetHashCode(); + return hashCode; + } + + /// + /// Returns a string representation of the value. + /// + /// A string representation of the value. + public override string ToString() => $"UserIdentity: {Id} ({Provider})"; + } +} diff --git a/Tests/Realm.Tests/Sync/UserManagementTests.cs b/Tests/Realm.Tests/Sync/UserManagementTests.cs index a4fc67b07d..f84d018324 100644 --- a/Tests/Realm.Tests/Sync/UserManagementTests.cs +++ b/Tests/Realm.Tests/Sync/UserManagementTests.cs @@ -103,6 +103,8 @@ public void AppSwitchUser_SwitchesCurrentUser() public void AppSwitchUser_WhenUserIsCurrent_DoesNothing() { var first = GetFakeUser(); + Assert.That(DefaultApp.CurrentUser, Is.EqualTo(first)); + var second = GetFakeUser(); Assert.That(DefaultApp.CurrentUser, Is.EqualTo(second)); @@ -194,15 +196,27 @@ public void User_LinkCredentials_AllowsLoginWithNewCredentials() SyncTestHelpers.RunBaasTestAsync(async () => { var user = await DefaultApp.LogInAsync(Credentials.Anonymous()); + + Assert.That(user.Identities, Has.Length.EqualTo(1)); + Assert.That(user.Identities[0].Provider, Is.EqualTo(Credentials.AuthProvider.Anonymous)); + Assert.That(user.Identities[0].Id, Is.Not.Null); + var email = SyncTestHelpers.GetVerifiedUsername(); await DefaultApp.EmailPasswordAuth.RegisterUserAsync(email, SyncTestHelpers.DefaultPassword); var linkedUser = await user.LinkCredentialsAsync(Credentials.EmailPassword(email, SyncTestHelpers.DefaultPassword)); + Assert.That(user.Identities, Has.Length.EqualTo(2)); + Assert.That(user.Identities[1].Provider, Is.EqualTo(Credentials.AuthProvider.EmailPassword)); + Assert.That(user.Identities[1].Id, Is.Not.Null); + + Assert.That(linkedUser.Identities, Has.Length.EqualTo(2)); Assert.That(linkedUser.Id, Is.EqualTo(user.Id)); + Assert.That(linkedUser.Identities, Is.EquivalentTo(user.Identities)); var emailPasswordUser = await DefaultApp.LogInAsync(Credentials.EmailPassword(email, SyncTestHelpers.DefaultPassword)); Assert.That(emailPasswordUser.Id, Is.EqualTo(user.Id)); + Assert.That(emailPasswordUser.Identities, Is.EquivalentTo(user.Identities)); }); } @@ -227,6 +241,16 @@ public void User_LinkCredentials_MultipleTimes_AllowsLoginWithAllCredentials() var functionUser = await DefaultApp.LogInAsync(Credentials.Function(new { realmCustomAuthFuncUserId = functionId })); Assert.That(functionUser.Id, Is.EqualTo(user.Id)); + + Assert.That(user.Identities, Has.Length.EqualTo(3)); + Assert.That(user.Identities[0].Provider, Is.EqualTo(Credentials.AuthProvider.Anonymous)); + Assert.That(user.Identities[0].Id, Is.Not.Null); + + Assert.That(user.Identities[1].Provider, Is.EqualTo(Credentials.AuthProvider.EmailPassword)); + Assert.That(user.Identities[1].Id, Is.Not.Null); + + Assert.That(user.Identities[2].Provider, Is.EqualTo(Credentials.AuthProvider.Function)); + Assert.That(user.Identities[2].Id, Is.EqualTo(functionId)); }); } @@ -246,6 +270,10 @@ public void User_LinkCredentials_MultipleTimesSameCredentials_IsNoOp() var functionUser = await DefaultApp.LogInAsync(Credentials.Function(new { realmCustomAuthFuncUserId = functionId })); Assert.That(functionUser.Id, Is.EqualTo(user.Id)); + + Assert.That(user.Identities, Has.Length.EqualTo(2)); + Assert.That(user.Identities[1].Id, Is.EqualTo(functionId)); + Assert.That(user.Identities[1].Provider, Is.EqualTo(Credentials.AuthProvider.Function)); }); } diff --git a/wrappers/src/sync_user_cs.cpp b/wrappers/src/sync_user_cs.cpp index 652b040767..bbd084d594 100644 --- a/wrappers/src/sync_user_cs.cpp +++ b/wrappers/src/sync_user_cs.cpp @@ -102,6 +102,54 @@ namespace realm { invoke_api_key_callback(tcs_ptr, api_keys, err); } + + inline AuthProvider to_auth_provider(const std::string& provider) { + if (provider == IdentityProviderAnonymous) { + return AuthProvider::ANONYMOUS; + } + + if (provider == IdentityProviderFacebook) { + return AuthProvider::FACEBOOK; + } + + if (provider == IdentityProviderGoogle) { + return AuthProvider::GOOGLE; + } + + if (provider == IdentityProviderApple) { + return AuthProvider::APPLE; + } + + if (provider == IdentityProviderCustom) { + return AuthProvider::CUSTOM; + } + + if (provider == IdentityProviderUsernamePassword) { + return AuthProvider::USERNAME_PASSWORD; + } + + if (provider == IdentityProviderFunction) { + return AuthProvider::FUNCTION; + } + + if (provider == IdentityProviderUserAPIKey) { + return AuthProvider::USER_API_KEY; + } + + if (provider == IdentityProviderServerAPIKey) { + return AuthProvider::SERVER_API_KEY; + } + + return (AuthProvider)999; + } + } + + void to_json(nlohmann::json& j, const SyncUserIdentity& i) + { + j = nlohmann::json{ + { "Id", i.id }, + { "Provider", to_auth_provider(i.provider_type)} + }; } } @@ -161,44 +209,7 @@ extern "C" { REALM_EXPORT AuthProvider realm_syncuser_get_auth_provider(SharedSyncUser& user, NativeException::Marshallable& ex) { return handle_errors(ex, [&] { - auto provider = user->provider_type(); - if (provider == IdentityProviderAnonymous) { - return AuthProvider::ANONYMOUS; - } - - if (provider == IdentityProviderFacebook) { - return AuthProvider::FACEBOOK; - } - - if (provider == IdentityProviderGoogle) { - return AuthProvider::GOOGLE; - } - - if (provider == IdentityProviderApple) { - return AuthProvider::APPLE; - } - - if (provider == IdentityProviderCustom) { - return AuthProvider::CUSTOM; - } - - if (provider == IdentityProviderUsernamePassword) { - return AuthProvider::USERNAME_PASSWORD; - } - - if (provider == IdentityProviderFunction) { - return AuthProvider::FUNCTION; - } - - if (provider == IdentityProviderUserAPIKey) { - return AuthProvider::USER_API_KEY; - } - - if (provider == IdentityProviderServerAPIKey) { - return AuthProvider::SERVER_API_KEY; - } - - return (AuthProvider)999; + return to_auth_provider(user->provider_type()); }); } @@ -280,6 +291,14 @@ extern "C" { }); } + REALM_EXPORT size_t realm_syncuser_get_serialized_identities(SharedSyncUser& user, uint16_t* string_buffer, size_t buffer_size, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() -> size_t { + nlohmann::json j = user->identities(); + return stringdata_to_csharpstringbuffer(j.dump(), string_buffer, buffer_size); + }); + } + REALM_EXPORT SharedApp* realm_syncuser_get_app(SharedSyncUser& user, NativeException::Marshallable& ex) { return handle_errors(ex, [&] {