From 5e0fb6f7649f024aa3cd0b95fe11446c8f95a4bf Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Thu, 31 Aug 2023 12:01:30 +0530 Subject: [PATCH] fix: multitenant user association with account linking (#777) * fix: test user association * fix: multitenancy related changes * fix: pr comments * fix: pr comments --- .../io/supertokens/authRecipe/AuthRecipe.java | 68 +-- .../emailpassword/EmailPassword.java | 13 +- .../java/io/supertokens/inmemorydb/Start.java | 72 +-- .../multitenancy/Multitenancy.java | 82 +++- .../passwordless/Passwordless.java | 13 +- .../io/supertokens/thirdparty/ThirdParty.java | 27 +- .../test/accountlinking/MultitenantTest.java | 427 ++++++++++++++++++ .../accountlinking/UnlinkAccountsTest.java | 39 +- .../api/TestRecipeUserIdInSignInUpAPIs.java | 5 + .../api/TestMultitenancyAPIHelper.java | 75 +++ .../api/TestTenantUserAssociation.java | 72 +++ 11 files changed, 800 insertions(+), 93 deletions(-) create mode 100644 src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index 4c07bdadf..75a28a173 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -244,7 +244,8 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection assert (recipeUser.loginMethods.length == 1); LoginMethod recipeUserIdLM = recipeUser.loginMethods[0]; - Set tenantIds = recipeUser.tenantIds; + Set tenantIds = new HashSet<>(); + tenantIds.addAll(recipeUser.tenantIds); tenantIds.addAll(primaryUser.tenantIds); // we loop through the union of both the user's tenantIds and check that the criteria for @@ -267,9 +268,12 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection if (recipeUserIdLM.email != null) { AuthRecipeUserInfo[] usersWithSameEmail = storage - .listPrimaryUsersByEmail_Transaction(tenantIdentifier, con, + .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con, recipeUserIdLM.email); for (AuthRecipeUserInfo user : usersWithSameEmail) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), "This user's email is already associated with another user ID"); @@ -279,9 +283,12 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection if (recipeUserIdLM.phoneNumber != null) { AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage - .listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifier, con, + .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con, recipeUserIdLM.phoneNumber); for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), "This user's phone number is already associated with another user" + @@ -291,16 +298,22 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection } if (recipeUserIdLM.thirdParty != null) { - AuthRecipeUserInfo userWithSameThirdParty = storage - .getPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifier, con, + AuthRecipeUserInfo[] usersWithSameThirdParty = storage + .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con, recipeUserIdLM.thirdParty.id, recipeUserIdLM.thirdParty.userId); - if (userWithSameThirdParty != null && userWithSameThirdParty.isPrimaryUser && - !userWithSameThirdParty.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { - throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( - userWithSameThirdParty.getSupertokensUserId(), - "This user's third party login is already associated with another" + - " user ID"); + for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) { + if (!userWithSameThirdParty.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameThirdParty.isPrimaryUser && + !userWithSameThirdParty.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + userWithSameThirdParty.getSupertokensUserId(), + "This user's third party login is already associated with another" + + " user ID"); + } } + } } @@ -461,18 +474,14 @@ private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionCon LoginMethod loginMethod = targetUser.loginMethods[0]; for (String tenantId : targetUser.tenantIds) { - TenantIdentifier tenantIdentifier = new TenantIdentifier( - appIdentifierWithStorage.getConnectionUriDomain(), appIdentifierWithStorage.getAppId(), - tenantId); - // we do not bother with getting the tenantIdentifierWithStorage here because - // we get the tenants from the user itself, and the user can only be shared across - // tenants of the same storage - therefore, the storage will be the same. - if (loginMethod.email != null) { AuthRecipeUserInfo[] usersWithSameEmail = storage - .listPrimaryUsersByEmail_Transaction(tenantIdentifier, con, + .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con, loginMethod.email); for (AuthRecipeUserInfo user : usersWithSameEmail) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } if (user.isPrimaryUser) { throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), "This user's email is already associated with another user ID"); @@ -482,7 +491,7 @@ private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionCon if (loginMethod.phoneNumber != null) { AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage - .listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifier, con, + .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con, loginMethod.phoneNumber); for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) { if (user.isPrimaryUser) { @@ -494,14 +503,19 @@ private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionCon } if (loginMethod.thirdParty != null) { - AuthRecipeUserInfo userWithSameThirdParty = storage - .getPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifier, con, + AuthRecipeUserInfo[] usersWithSameThirdParty = storage + .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con, loginMethod.thirdParty.id, loginMethod.thirdParty.userId); - if (userWithSameThirdParty != null && userWithSameThirdParty.isPrimaryUser) { - throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( - userWithSameThirdParty.getSupertokensUserId(), - "This user's third party login is already associated with another" + - " user ID"); + for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) { + if (!userWithSameThirdParty.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameThirdParty.isPrimaryUser) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + userWithSameThirdParty.getSupertokensUserId(), + "This user's third party login is already associated with another" + + " user ID"); + } } } } diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java index 60141104f..950b9bf3e 100644 --- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java +++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java @@ -603,20 +603,15 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti if (email != null) { if (user.isPrimaryUser) { for (String tenantId : user.tenantIds) { - // we do not bother with getting the tenantIdentifierWithStorage here because - // we get the tenants from the user itself, and the user can only be shared across - // tenants of the same storage - therefore, the storage will be the same. - TenantIdentifier tenantIdentifier = new TenantIdentifier( - appIdentifierWithStorage.getConnectionUriDomain(), - appIdentifierWithStorage.getAppId(), - tenantId); - AuthRecipeUserInfo[] existingUsersWithNewEmail = authRecipeStorage.listPrimaryUsersByEmail_Transaction( - tenantIdentifier, transaction, + appIdentifierWithStorage, transaction, email); for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) { throw new StorageTransactionLogicException( new EmailChangeNotAllowedException()); diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 0241be936..7820d2b26 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -57,6 +57,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -102,7 +103,7 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - MultitenancyStorage, TOTPSQLStorage, ActiveUsersStorage, DashboardSQLStorage, AuthRecipeSQLStorage { + MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage, DashboardSQLStorage, AuthRecipeSQLStorage { private static final Object appenderLock = new Object(); private static final String APP_ID_KEY_NAME = "app_id"; @@ -2245,9 +2246,9 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) - throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, - DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection conn, String userId) + throws StorageQueryException, TenantOrAppNotFoundException, DuplicateEmailException, + DuplicateThirdPartyUserException, DuplicatePhoneNumberException, UnknownUserIdException { try { return this.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); @@ -2758,44 +2759,47 @@ public AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdenti } @Override - public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(TenantIdentifier tenantIdentifier, + public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String email) throws StorageQueryException { - try { - Connection sqlCon = (Connection) con.getConnection(); - return GeneralQueries.listPrimaryUsersByEmail_Transaction(this, sqlCon, tenantIdentifier, email); - } catch (SQLException e) { - throw new StorageQueryException(e); - } + return null; // TODO +// try { +// Connection sqlCon = (Connection) con.getConnection(); +// return GeneralQueries.listPrimaryUsersByEmail_Transaction(this, sqlCon, appIdentifier, email); +// } catch (SQLException e) { +// throw new StorageQueryException(e); +// } } @Override - public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(TenantIdentifier tenantIdentifier, + public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String phoneNumber) throws StorageQueryException { - try { - Connection sqlCon = (Connection) con.getConnection(); - return GeneralQueries.listPrimaryUsersByPhoneNumber_Transaction(this, sqlCon, tenantIdentifier, - phoneNumber); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public AuthRecipeUserInfo getPrimaryUsersByThirdPartyInfo_Transaction(TenantIdentifier tenantIdentifier, - TransactionConnection con, - String thirdPartyId, - String thirdPartyUserId) - throws StorageQueryException { - try { - Connection sqlCon = (Connection) con.getConnection(); - return GeneralQueries.getPrimaryUsersByThirdPartyInfo_Transaction(this, sqlCon, tenantIdentifier, - thirdPartyId, thirdPartyUserId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } + return null; // TODO +// try { +// Connection sqlCon = (Connection) con.getConnection(); +// return GeneralQueries.listPrimaryUsersByPhoneNumber_Transaction(this, sqlCon, tenantIdentifier, +// phoneNumber); +// } catch (SQLException e) { +// throw new StorageQueryException(e); +// } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + return null; // TODO +// try { +// Connection sqlCon = (Connection) con.getConnection(); +// return GeneralQueries.getPrimaryUsersByThirdPartyInfo_Transaction(this, sqlCon, tenantIdentifier, +// thirdPartyId, thirdPartyUserId); +// } catch (SQLException e) { +// throw new StorageQueryException(e); +// } } @Override diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 21fe432cb..03858a5c0 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -28,15 +28,20 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.*; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; @@ -385,8 +390,81 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t throw new FeatureNotEnabledException(EE_FEATURES.MULTI_TENANCY); } - return tenantIdentifierWithStorage.getMultitenancyStorageWithTargetStorage() - .addUserIdToTenant(tenantIdentifierWithStorage, userId); + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); + try { + return storage.startTransaction(con -> { + String tenantId = tenantIdentifierWithStorage.getTenantId(); + AuthRecipeUserInfo userToAssociate = storage.getPrimaryUserById_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, userId); + + if (userToAssociate.isPrimaryUser) { + Set emails = new HashSet<>(); + Set phoneNumbers = new HashSet<>(); + Set thirdParties = new HashSet<>(); + + // Loop through all the emails, phoneNumbers and thirdPartyInfos and check for conflicts + for (LoginMethod lM : userToAssociate.loginMethods) { + if (lM.email != null) { + emails.add(lM.email); + } + if (lM.phoneNumber != null) { + phoneNumbers.add(lM.phoneNumber); + } + if (lM.thirdParty != null) { + thirdParties.add(lM.thirdParty); + } + } + + for (String email : emails) { + AuthRecipeUserInfo[] users = storage.listPrimaryUsersByEmail_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, email); + for (AuthRecipeUserInfo user : users) { + if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { + throw new StorageTransactionLogicException(new DuplicateEmailException()); + } + } + } + + for (String phoneNumber : phoneNumbers) { + AuthRecipeUserInfo[] users = storage.listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, phoneNumber); + for (AuthRecipeUserInfo user : users) { + if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { + throw new StorageTransactionLogicException(new DuplicatePhoneNumberException()); + } + } + } + + for (LoginMethod.ThirdParty tp : thirdParties) { + AuthRecipeUserInfo[] users = storage.listPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, tp.id, tp.userId); + for (AuthRecipeUserInfo user : users) { + if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { + throw new StorageTransactionLogicException(new DuplicateThirdPartyUserException()); + } + } + } + } + + try { + boolean result = ((MultitenancySQLStorage) storage).addUserIdToTenant_Transaction(tenantIdentifierWithStorage, con, userId); + storage.commitTransaction(con); + return result; + } catch (TenantOrAppNotFoundException | UnknownUserIdException | DuplicatePhoneNumberException | + DuplicateThirdPartyUserException | DuplicateEmailException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof DuplicateEmailException) { + throw (DuplicateEmailException) e.actualException; + } else if (e.actualException instanceof DuplicatePhoneNumberException) { + throw (DuplicatePhoneNumberException) e.actualException; + } else if (e.actualException instanceof DuplicateThirdPartyUserException) { + throw (DuplicateThirdPartyUserException) e.actualException; + } else if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } + throw new StorageQueryException(e.actualException); + } } public static boolean removeUserIdFromTenant(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage, diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index e6acfb4b4..d0b06dc0b 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -710,20 +710,15 @@ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, if (emailUpdate != null && !Objects.equals(emailUpdate.newValue, lM.email)) { if (user.isPrimaryUser) { for (String tenantId : user.tenantIds) { - // we do not bother with getting the tenantIdentifierWithStorage here because - // we get the tenants from the user itself, and the user can only be shared across - // tenants of the same storage - therefore, the storage will be the same. - TenantIdentifier tenantIdentifier = new TenantIdentifier( - appIdentifierWithStorage.getConnectionUriDomain(), - appIdentifierWithStorage.getAppId(), - tenantId); - AuthRecipeUserInfo[] existingUsersWithNewEmail = authRecipeSQLStorage.listPrimaryUsersByEmail_Transaction( - tenantIdentifier, con, + appIdentifierWithStorage, con, emailUpdate.newValue); for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) { throw new StorageTransactionLogicException( new EmailChangeNotAllowedException()); diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index da6629b4a..366002d6e 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -217,10 +217,20 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); storage.startTransaction(con -> { - AuthRecipeUserInfo userFromDb = authRecipeStorage.getPrimaryUsersByThirdPartyInfo_Transaction( - tenantIdentifierWithStorage, + AuthRecipeUserInfo userFromDb = null; + + AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo_Transaction( + appIdentifier, con, thirdPartyId, thirdPartyUserId); + for (AuthRecipeUserInfo user : usersFromDb) { + if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + if (userFromDb != null) { + throw new IllegalStateException("Should never happen"); + } + userFromDb = user; + } + } if (userFromDb == null) { storage.commitTransaction(con); @@ -246,19 +256,14 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan // email, and if they do, then we do not allow the update. if (userFromDb.isPrimaryUser) { for (String tenantId : userFromDb.tenantIds) { - // we do not bother with getting the tenantIdentifierWithStorage here because - // we get the tenants from the user itself, and the user can only be shared across - // tenants of the same storage - therefore, the storage will be the same. - TenantIdentifier tenantIdentifier = new TenantIdentifier( - tenantIdentifierWithStorage.getConnectionUriDomain(), - tenantIdentifierWithStorage.getAppId(), - tenantId); - AuthRecipeUserInfo[] userBasedOnEmail = authRecipeStorage.listPrimaryUsersByEmail_Transaction( - tenantIdentifier, con, email + appIdentifier, con, email ); for (AuthRecipeUserInfo userWithSameEmail : userBasedOnEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(userFromDb.getSupertokensUserId())) { throw new StorageTransactionLogicException( diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java new file mode 100644 index 000000000..f9cdc8e12 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * 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. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.function.Function; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class MultitenantTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + TenantIdentifier t1, t2, t3; + + private void createTenants(Main main) + throws StorageQueryException, TenantOrAppNotFoundException, InvalidProviderConfigException, + FeatureNotEnabledException, IOException, InvalidConfigException, + CannotModifyBaseConfigException, BadPermissionException { + // User pool 1 - (null, a1, null) + // User pool 2 - (null, a1, t1), (null, a1, t2) + + { // tenant 1 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + { // tenant 2 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t1"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + { // tenant 3 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t2"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + t3 = new TenantIdentifier(null, "a1", "t2"); + } + + @Test + public void testWithEmailPasswordUsers1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password"); + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + try { + // Credentials does not exist in t2 + EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + fail(); + } catch (WrongCredentialsException e) { + // ignore + } + + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); + + // Sign in should now pass + EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithEmailPasswordUsers2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password"); + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + try { + // Credentials does not exist in t2 + EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + fail(); + } catch (WrongCredentialsException e) { + // ignore + } + + // same email is allowed to sign up + AuthRecipeUserInfo user3 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + + // Sign in should pass + EmailPassword.signIn(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + + try { + // user 3 cannot become a primary user + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + fail(); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + // Ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithEmailPasswordUsersAndPasswordlessUser1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); + Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t2WithStorage, process.getProcess(), + "test2@example.com", null, null, null); + AuthRecipeUserInfo user2 = Passwordless.consumeCode(t2WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user; + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + try { + // Credentials does not exist in t2 + EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + fail(); + } catch (WrongCredentialsException e) { + // ignore + } + + // same email is allowed to sign up + AuthRecipeUserInfo user3 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + + // Sign in should pass + EmailPassword.signIn(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + + try { + // user 3 cannot become a primary user + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + fail(); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + // Ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithEmailPasswordUsersAndPasswordlessUser2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); + Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t2WithStorage, process.getProcess(), + "test2@example.com", null, null, null); + AuthRecipeUserInfo user2 = Passwordless.consumeCode(t2WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user; + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + // same email is allowed to sign in up + Passwordless.CreateCodeResponse user3code = Passwordless.createCode(t1WithStorage, process.getProcess(), + "test2@example.com", null, null, null); + + AuthRecipeUserInfo user3 = Passwordless.consumeCode(t1WithStorage, process.getProcess(), user3code.deviceId, user3code.deviceIdHash, user3code.userInputCode, null).user; + + try { + // user 3 cannot become a primary user + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + fail(); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + // Ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithEmailPasswordUserAndThirdPartyUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "google-user", "test2@example.com").user; + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + try { + // Credentials does not exist in t2 + EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + fail(); + } catch (WrongCredentialsException e) { + // ignore + } + + // same email is allowed to sign up + AuthRecipeUserInfo user3 = ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "google-user", "test2@example.com").user; + + try { + // user 3 cannot become a primary user + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + fail(); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + // Ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testTenantAssociationWithEPUsers1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + TenantIdentifierWithStorage t3WithStorage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password2"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(t3WithStorage, process.getProcess(), "test1@example.com", "password3"); + + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user3.getSupertokensUserId()); + try { + AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + fail(); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + // ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testTenantAssociationWithEPUsers2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + TenantIdentifierWithStorage t3WithStorage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password2"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(t3WithStorage, process.getProcess(), "test1@example.com", "password3"); + + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); + try { + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user3.getSupertokensUserId()); + fail(); + } catch (DuplicateEmailException e) { + // ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java index 78a2f4a8e..5a9d7cb74 100644 --- a/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java @@ -36,7 +36,7 @@ import org.junit.Test; import org.junit.rules.TestRule; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; public class UnlinkAccountsTest { @@ -248,4 +248,41 @@ public void unlinkAccountSuccessButDeletesUser() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testUnlinkUserDeletesRecipeUserAndAnotherUserLinkToIt() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + AuthRecipe.unlinkAccounts(process.getProcess(), user1.getSupertokensUserId()); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.getProcess(), user2.getSupertokensUserId()); + assertEquals(refetchUser2.getSupertokensUserId(), user1.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), user2.getSupertokensUserId()); + AuthRecipeUserInfo refetchUser3 = AuthRecipe.getUserById(process.getProcess(), user3.getSupertokensUserId()); + assertEquals(refetchUser3.getSupertokensUserId(), user1.getSupertokensUserId()); + + assertEquals(refetchUser3.loginMethods.length, 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } } diff --git a/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java index acbadcf76..b09b69976 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java @@ -236,6 +236,7 @@ public void testThirdPartySignInUp() throws Exception { { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -255,6 +256,7 @@ public void testThirdPartySignInUp() throws Exception { // Without account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -279,6 +281,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -300,6 +303,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -317,6 +321,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "facebook"); diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java index 8d5ff811c..2338576ed 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Random; import static org.junit.Assert.assertEquals; @@ -315,6 +316,80 @@ public static JsonObject tpSignInUp(TenantIdentifier tenantIdentifier, String th return response.get("user").getAsJsonObject(); } + private static String generateRandomString(int length) { + StringBuilder sb = new StringBuilder(length); + final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final Random RANDOM = new Random(); + for (int i = 0; i < length; i++) { + int randomIndex = RANDOM.nextInt(ALPHABET.length()); + char randomChar = ALPHABET.charAt(randomIndex); + sb.append(randomChar); + } + return sb.toString(); + } + + private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, String email, Main main) + throws HttpResponseException, IOException { + String exampleCode = generateRandomString(6); + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("email", email); + createCodeRequestBody.addProperty("userInputCode", exampleCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code"), + createCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(8, response.entrySet().size()); + + return response; + } + + private static JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, + String userInputCode, Main main) + throws HttpResponseException, IOException { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", preAuthSessionId); + consumeCodeRequestBody.addProperty("userInputCode", userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code/consume"), + consumeCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + assertEquals("OK", response.get("status").getAsString()); + return response.get("user").getAsJsonObject(); + } + + public static JsonObject plSignInUpEmail(TenantIdentifier tenantIdentifier, String email, Main main) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithEmail(tenantIdentifier, email, main); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main); + } + + private static JsonObject createCodeWithNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main) + throws HttpResponseException, IOException { + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("phoneNumber", phoneNumber); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code"), + createCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(8, response.entrySet().size()); + + return response; + } + + public static JsonObject plSignInUpNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber, main); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main); + } + public static void addLicense(String licenseKey, Main main) throws HttpResponseException, IOException { JsonObject licenseKeyRequest = new JsonObject(); licenseKeyRequest.addProperty("licenseKey", licenseKey); diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java index c95a5d84d..9c86c127e 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java @@ -501,4 +501,76 @@ public void testDisassociateUserWithUserIdMappingAndSession() throws Exception { // OK } } + + @Test + public void testThatUserWithSameEmailCannotBeAssociatedToATenantForEp() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t1"), "user@example.com", + "password", process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t2"), "user@example.com", + "password", process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("EMAIL_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSameThirdPartyInfoCannotBeAssociatedToATenantForTp() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.tpSignInUp(new TenantIdentifier(null, "a1", "t1"), "google", "google-user", "user@example.com", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.tpSignInUp(new TenantIdentifier(null, "a1", "t2"), "google", "google-user", "user@example.com", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("THIRD_PARTY_USER_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSameEmailCannotBeAssociatedToATenantForPless() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.plSignInUpEmail(new TenantIdentifier(null, "a1", "t1"), "user@example.com", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.plSignInUpEmail(new TenantIdentifier(null, "a1", "t2"), "user@example.com", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("EMAIL_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSamePhoneCannotBeAssociatedToATenantForPless() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.plSignInUpNumber(new TenantIdentifier(null, "a1", "t1"), "+919876543210", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.plSignInUpNumber(new TenantIdentifier(null, "a1", "t2"), "+919876543210", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("PHONE_NUMBER_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } }