From 21dafb1b748c16094ebf952dcee85730f570693f Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 12 May 2023 16:43:17 +0530 Subject: [PATCH] fix: resource reloading (#673) * fix: resource reloading * fix: license test task reloading * fix: reload revert * fix: impl * fix: pr comments * fix: pr comments * fix: pr comment * fix: pr comments --- .../java/io/supertokens/ee/EEFeatureFlag.java | 6 +- .../ee/cronjobs/EELicenseCheck.java | 29 +- src/main/java/io/supertokens/Main.java | 7 +- .../java/io/supertokens/ProcessState.java | 2 +- .../io/supertokens/ResourceDistributor.java | 25 +- .../java/io/supertokens/config/Config.java | 36 +- .../io/supertokens/cronjobs/Cronjobs.java | 9 +- .../emailpassword/EmailPassword.java | 3 - .../emailverification/EmailVerification.java | 3 - .../supertokens/featureflag/FeatureFlag.java | 7 +- .../multitenancy/Multitenancy.java | 19 +- .../multitenancy/MultitenancyHelper.java | 85 ++-- .../session/refreshToken/RefreshTokenKey.java | 8 +- .../signingkeys/AccessTokenSigningKey.java | 8 +- .../signingkeys/JWTSigningKey.java | 8 +- .../supertokens/signingkeys/SigningKeys.java | 8 +- .../storageLayer/StorageLayer.java | 5 + .../java/io/supertokens/test/CronjobTest.java | 19 + .../io/supertokens/test/PathRouterTest.java | 5 +- ...ExpiredPasswordResetTokensCronjobTest.java | 20 +- .../test/emailpassword/EmailPasswordTest.java | 3 +- ...redEmailVerificationTokensCronjobTest.java | 20 +- .../EmailVerificationTest.java | 4 +- .../test/multitenant/ConfigTest.java | 395 +++++++++++++++++- .../test/multitenant/SigningKeysTest.java | 4 +- .../userIdMapping/api/MultitenantAPITest.java | 50 ++- 26 files changed, 652 insertions(+), 136 deletions(-) diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 84a52d417..5cd2c83cc 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -94,7 +94,11 @@ public void updateEnabledFeaturesValueReadFromDbTime(long newTime) { public void constructor(Main main, AppIdentifier appIdentifier) { this.main = main; this.appIdentifier = appIdentifier; - Cronjobs.addCronjob(main, EELicenseCheck.getInstance(main, this.appIdentifier.getAsPublicTenantIdentifier())); + + // EELicenseCheck.init does not create a new CronTask each time, it creates for the first time and + // returns the same instance from there on. + Cronjobs.addCronjob(main, EELicenseCheck.init(main, StorageLayer.getTenantsWithUniqueUserPoolId(main))); + try { this.syncFeatureFlagWithLicenseKey(); } catch (HttpResponseException | IOException e) { diff --git a/ee/src/main/java/io/supertokens/ee/cronjobs/EELicenseCheck.java b/ee/src/main/java/io/supertokens/ee/cronjobs/EELicenseCheck.java index d19fa8609..d186545e1 100644 --- a/ee/src/main/java/io/supertokens/ee/cronjobs/EELicenseCheck.java +++ b/ee/src/main/java/io/supertokens/ee/cronjobs/EELicenseCheck.java @@ -3,34 +3,33 @@ import io.supertokens.Main; import io.supertokens.cronjobs.CronTask; import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.cronjobs.Cronjobs; +import io.supertokens.cronjobs.deleteExpiredAccessTokenSigningKeys.DeleteExpiredAccessTokenSigningKeys; import io.supertokens.ee.EEFeatureFlag; import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storageLayer.StorageLayer; + +import java.util.List; public class EELicenseCheck extends CronTask { public static final String RESOURCE_KEY = "io.supertokens.ee.cronjobs.EELicenseCheck"; - private EELicenseCheck(Main main, TenantIdentifier targetTenant) { - super("EELicenseCheck", main, targetTenant); + private EELicenseCheck(Main main, List> tenantsInfo) { + super("EELicenseCheck", main, tenantsInfo, true); } - public static EELicenseCheck getInstance(Main main, TenantIdentifier tenantIdentifier) { - try { - return (EELicenseCheck) main.getResourceDistributor() - .getResource(tenantIdentifier, RESOURCE_KEY); - } catch (TenantOrAppNotFoundException e) { - return (EELicenseCheck) main.getResourceDistributor() - .setResource(tenantIdentifier, RESOURCE_KEY, - new EELicenseCheck(main, tenantIdentifier)); - } + public static EELicenseCheck init(Main main, List> tenantsInfo) { + return (EELicenseCheck) main.getResourceDistributor() + .setResource(new TenantIdentifier(null, null, null), RESOURCE_KEY, + new EELicenseCheck(main, tenantsInfo)); } @Override - protected void doTaskForTargetTenant(TenantIdentifier tenantIdentifier) throws Exception { - // this cronjob is for one tenant only (the targetTenant provided in the constructor) - FeatureFlag.getInstance(main, tenantIdentifier.toAppIdentifier()).syncFeatureFlagWithLicenseKey(); + protected void doTaskPerApp(AppIdentifier app) throws Exception { + FeatureFlag.getInstance(main, app).syncFeatureFlagWithLicenseKey(); } @Override diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index b75838010..dbeccaa34 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -55,6 +55,7 @@ import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -205,7 +206,7 @@ private void init() throws IOException, StorageQueryException { try { // load all configs for each of the tenants. - MultitenancyHelper.getInstance(this).loadConfig(); + MultitenancyHelper.getInstance(this).loadConfig(new ArrayList<>()); // init storage layers for each unique db connection based on unique (user pool ID, connection pool ID). MultitenancyHelper.getInstance(this).loadStorageLayer(); @@ -214,11 +215,11 @@ private void init() throws IOException, StorageQueryException { } // load feature flag for all loaded apps - MultitenancyHelper.getInstance(this).loadFeatureFlag(); + MultitenancyHelper.getInstance(this).loadFeatureFlag(new ArrayList<>()); // init signing keys try { - MultitenancyHelper.getInstance(this).loadSigningKeys(); + MultitenancyHelper.getInstance(this).loadSigningKeys(new ArrayList<>()); } catch (UnsupportedJWTSigningAlgorithmException e) { throw new QuitProgramException(e); } diff --git a/src/main/java/io/supertokens/ProcessState.java b/src/main/java/io/supertokens/ProcessState.java index b1ed547de..254364b35 100644 --- a/src/main/java/io/supertokens/ProcessState.java +++ b/src/main/java/io/supertokens/ProcessState.java @@ -92,7 +92,7 @@ public enum PROCESS_STATE { PASSWORD_HASH_BCRYPT, PASSWORD_HASH_ARGON, PASSWORD_VERIFY_BCRYPT, PASSWORD_VERIFY_ARGON, PASSWORD_VERIFY_FIREBASE_SCRYPT, ADDING_REMOTE_ADDRESS_FILTER, LICENSE_KEY_CHECK_NETWORK_CALL, INVALID_LICENSE_KEY, SERVER_ERROR_DURING_LICENSE_KEY_CHECK_FAIL, LOADING_ALL_TENANT_CONFIG, - LOADING_ALL_TENANT_STORAGE + LOADING_ALL_TENANT_STORAGE, TENANTS_CHANGED_DURING_REFRESH_FROM_DB } public static class EventAndException { diff --git a/src/main/java/io/supertokens/ResourceDistributor.java b/src/main/java/io/supertokens/ResourceDistributor.java index 24ca4ae2a..82f5caa91 100644 --- a/src/main/java/io/supertokens/ResourceDistributor.java +++ b/src/main/java/io/supertokens/ResourceDistributor.java @@ -58,7 +58,7 @@ public synchronized SingletonResource getResource(TenantIdentifier tenantIdentif throw new TenantOrAppNotFoundException(tenantIdentifier); } - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); // we try again.. resource = resources.get(new KeyClass(tenantIdentifier, key)); @@ -104,12 +104,27 @@ public synchronized SingletonResource setResource(TenantIdentifier tenantIdentif return resource; } + public synchronized SingletonResource removeResource(TenantIdentifier tenantIdentifier, + @Nonnull String key) { + SingletonResource singletonResource = resources.get(new KeyClass(tenantIdentifier, key)); + if (singletonResource == null) { + return null; + } + resources.remove(new KeyClass(tenantIdentifier, key)); + return singletonResource; + } + public synchronized SingletonResource setResource(AppIdentifier appIdentifier, @Nonnull String key, SingletonResource resource) { return setResource(appIdentifier.getAsPublicTenantIdentifier(), key, resource); } + public synchronized SingletonResource removeResource(AppIdentifier appIdentifier, + @Nonnull String key) { + return removeResource(appIdentifier.getAsPublicTenantIdentifier(), key); + } + public synchronized void clearAllResourcesWithResourceKey(String inputKey) { List toRemove = new ArrayList<>(); resources.forEach((key, value) -> { @@ -138,12 +153,12 @@ public synchronized SingletonResource setResource(@Nonnull String key, return setResource(new TenantIdentifier(null, null, null), key, resource); } - public interface Func { - void performTask() throws FuncException; + public interface Func { + T performTask() throws FuncException; } - public synchronized void withResourceDistributorLock(Func func) throws FuncException { - func.performTask(); + public synchronized T withResourceDistributorLock(Func func) throws FuncException { + return func.performTask(); } public interface FuncWithReturn { diff --git a/src/main/java/io/supertokens/config/Config.java b/src/main/java/io/supertokens/config/Config.java index 3eb163d5c..9101b8b69 100644 --- a/src/main/java/io/supertokens/config/Config.java +++ b/src/main/java/io/supertokens/config/Config.java @@ -25,7 +25,6 @@ import io.supertokens.ResourceDistributor; import io.supertokens.cliOptions.CLIOptions; import io.supertokens.output.Logging; -import io.supertokens.pluginInterface.LOG_LEVEL; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.multitenancy.TenantConfig; @@ -36,9 +35,10 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Set; public class Config extends ResourceDistributor.SingletonResource { @@ -62,7 +62,7 @@ private Config(Main main, JsonObject jsonConfig) throws IOException, InvalidConf this.core = config; } - private static Config getInstance(TenantIdentifier tenantIdentifier, Main main) + public static Config getInstance(TenantIdentifier tenantIdentifier, Main main) throws TenantOrAppNotFoundException { return (Config) main.getResourceDistributor().getResource(tenantIdentifier, RESOURCE_KEY); } @@ -98,8 +98,14 @@ private static String getConfigFilePath(Main main) { : CLIOptions.get(main).getConfigFilePath(); } + @TestOnly public static void loadAllTenantConfig(Main main, TenantConfig[] tenants) throws IOException, InvalidConfigException { + loadAllTenantConfig(main, tenants, new ArrayList<>()); + } + + public static void loadAllTenantConfig(Main main, TenantConfig[] tenants, List tenantsThatChanged) + throws IOException, InvalidConfigException { ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.LOADING_ALL_TENANT_CONFIG, null); Map normalisedConfigs = getNormalisedConfigsForAllTenants( tenants, @@ -110,16 +116,32 @@ public static void loadAllTenantConfig(Main main, TenantConfig[] tenants) // At this point, we know that all configs are valid. try { main.getResourceDistributor().withResourceDistributorLock(() -> { - main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY); try { + Map existingResources = + main.getResourceDistributor() + .getAllResourcesWithResourceKey(RESOURCE_KEY); + main.getResourceDistributor().clearAllResourcesWithResourceKey(RESOURCE_KEY); for (ResourceDistributor.KeyClass key : normalisedConfigs.keySet()) { - main.getResourceDistributor() - .setResource(key.getTenantIdentifier(), RESOURCE_KEY, - new Config(main, normalisedConfigs.get(key))); + ResourceDistributor.SingletonResource resource = existingResources.get( + new ResourceDistributor.KeyClass( + key.getTenantIdentifier(), + RESOURCE_KEY)); + if (resource != null && !tenantsThatChanged.contains(key.getTenantIdentifier())) { + main.getResourceDistributor() + .setResource(key.getTenantIdentifier(), + RESOURCE_KEY, + resource); + } else { + main.getResourceDistributor() + .setResource(key.getTenantIdentifier(), RESOURCE_KEY, + new Config(main, normalisedConfigs.get(key))); + + } } } catch (InvalidConfigException | IOException e) { throw new ResourceDistributor.FuncException(e); } + return null; }); } catch (ResourceDistributor.FuncException e) { if (e.getCause() instanceof InvalidConfigException) { diff --git a/src/main/java/io/supertokens/cronjobs/Cronjobs.java b/src/main/java/io/supertokens/cronjobs/Cronjobs.java index 46acbf2cf..c25257b51 100644 --- a/src/main/java/io/supertokens/cronjobs/Cronjobs.java +++ b/src/main/java/io/supertokens/cronjobs/Cronjobs.java @@ -20,6 +20,7 @@ import io.supertokens.ResourceDistributor; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import org.jetbrains.annotations.TestOnly; import java.util.ArrayList; import java.util.List; @@ -92,8 +93,14 @@ public static void addCronjob(Main main, CronTask task) { synchronized (instance.lock) { instance.executor.scheduleWithFixedDelay(task, task.getInitialWaitTimeSeconds(), task.getIntervalTimeSeconds(), TimeUnit.SECONDS); - instance.tasks.add(task); + if (!instance.tasks.contains(task)) { + instance.tasks.add(task); + } } } + @TestOnly + public List getTasks() { + return this.tasks; + } } diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java index f0dd8adba..362772188 100644 --- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java +++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java @@ -69,9 +69,6 @@ public static long getPasswordResetTokenLifetimeForTests(Main main) { private static long getPasswordResetTokenLifetime(TenantIdentifier tenantIdentifier, Main main) throws TenantOrAppNotFoundException { - if (Main.isTesting) { - return EmailPasswordTest.getInstance(main).getPasswordResetTokenLifetime(); - } return Config.getConfig(tenantIdentifier, main).getPasswordResetTokenLifetime(); } diff --git a/src/main/java/io/supertokens/emailverification/EmailVerification.java b/src/main/java/io/supertokens/emailverification/EmailVerification.java index f2b0996cb..14bcca800 100644 --- a/src/main/java/io/supertokens/emailverification/EmailVerification.java +++ b/src/main/java/io/supertokens/emailverification/EmailVerification.java @@ -53,9 +53,6 @@ public static long getEmailVerificationTokenLifetimeForTests(Main main) { private static long getEmailVerificationTokenLifetime(TenantIdentifier tenantIdentifier, Main main) throws TenantOrAppNotFoundException { - if (Main.isTesting) { - return EmailVerificationTest.getInstance(main).getEmailVerificationTokenLifetime(); - } return Config.getConfig(tenantIdentifier, main).getEmailVerificationTokenLifetime(); } diff --git a/src/main/java/io/supertokens/featureflag/FeatureFlag.java b/src/main/java/io/supertokens/featureflag/FeatureFlag.java index 1641b600c..4fb54238a 100644 --- a/src/main/java/io/supertokens/featureflag/FeatureFlag.java +++ b/src/main/java/io/supertokens/featureflag/FeatureFlag.java @@ -122,7 +122,7 @@ public static void initForBaseTenant(Main main, String eeFolderPath) throws Malf new FeatureFlag(main, eeFolderPath)); } - public static void loadForAllTenants(Main main, List apps) { + public static void loadForAllTenants(Main main, List apps, List tenantsThatChanged) { try { main.getResourceDistributor().withResourceDistributorLock(() -> { Map existingResources = @@ -134,7 +134,7 @@ public static void loadForAllTenants(Main main, List apps) { new ResourceDistributor.KeyClass( app, RESOURCE_KEY)); - if (resource != null) { + if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) { main.getResourceDistributor() .setResource(app, RESOURCE_KEY, @@ -147,6 +147,7 @@ public static void loadForAllTenants(Main main, List apps) { new FeatureFlag(main, app)); } } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new RuntimeException(e); @@ -217,4 +218,4 @@ public static void clearURLClassLoader() { FeatureFlag.ucl = null; } } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index c5a10b151..51d094fc1 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -198,6 +198,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan validateTenantConfig(main, newTenant, shouldPreventDbConfigUpdate); boolean creationInSharedDbSucceeded = false; + List tenantsThatChanged = new ArrayList<>(); try { StorageLayer.getMultitenancyStorage(main).createTenant(newTenant); creationInSharedDbSucceeded = true; @@ -206,7 +207,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan // the tenant being there in the tenants table. But that insertion is done in the addTenantIdInUserPool // function below. So in order to actually refresh the resources, we have a finally block here which // calls the forceReloadAllResources function. - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(false); + tenantsThatChanged = MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(false); try { ((MultitenancyStorage) StorageLayer.getStorage(newTenant.tenantIdentifier, main)) .addTenantIdInTargetStorage(newTenant.tenantIdentifier); @@ -219,7 +220,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan if (!creationInSharedDbSucceeded) { try { StorageLayer.getMultitenancyStorage(main).overwriteTenantConfig(newTenant); - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(false); + tenantsThatChanged = MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(false); // we do this extra step cause if previously an attempt to add a tenant failed midway, // such that the main tenant was added in the user pool, but did not get created @@ -252,7 +253,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan } catch (DuplicateClientTypeException e) { throw new InvalidProviderConfigException("Duplicate clientType was specified in the clients list."); } finally { - MultitenancyHelper.getInstance(main).forceReloadAllResources(); + MultitenancyHelper.getInstance(main).forceReloadAllResources(tenantsThatChanged); } } @@ -269,7 +270,7 @@ public static boolean deleteTenant(TenantIdentifier tenantIdentifier, Main main) // but not from the main table. } boolean didExist = StorageLayer.getMultitenancyStorage(main).deleteTenantInfoInBaseStorage(tenantIdentifier); - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); return didExist; } @@ -291,7 +292,7 @@ public static boolean deleteApp(AppIdentifier appIdentifier, Main main) // but not from the main table. } boolean didExist = StorageLayer.getMultitenancyStorage(main).deleteAppInfoInBaseStorage(appIdentifier); - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); return didExist; } @@ -321,7 +322,7 @@ public static boolean deleteConnectionUriDomain(String connectionUriDomain, Main // but not from the main table. } boolean didExist = StorageLayer.getMultitenancyStorage(main).deleteConnectionUriDomainInfoInBaseStorage(connectionUriDomain); - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); return didExist; } @@ -371,7 +372,7 @@ public static TenantConfig getTenantInfo(Main main, TenantIdentifier tenantIdent } public static TenantConfig[] getAllTenantsForApp(AppIdentifier appIdentifier, Main main) { - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); TenantConfig[] tenants = MultitenancyHelper.getInstance(main).getAllTenants(); List tenantList = new ArrayList<>(); @@ -393,7 +394,7 @@ public static TenantConfig[] getAllAppsAndTenantsForConnectionUriDomain(String c if (connectionUriDomain == null) { connectionUriDomain = TenantIdentifier.DEFAULT_CONNECTION_URI; } - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); TenantConfig[] tenants = MultitenancyHelper.getInstance(main).getAllTenants(); List tenantList = new ArrayList<>(); @@ -412,7 +413,7 @@ public static TenantConfig[] getAllAppsAndTenantsForConnectionUriDomain(String c } public static TenantConfig[] getAllTenants(Main main) { - MultitenancyHelper.getInstance(main).refreshTenantsInCoreIfRequired(true); + MultitenancyHelper.getInstance(main).refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(true); return MultitenancyHelper.getInstance(main).getAllTenants(); } } diff --git a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java index a16a5a197..c88701c86 100644 --- a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java +++ b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java @@ -18,6 +18,7 @@ import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.ProcessState; import io.supertokens.ResourceDistributor; import io.supertokens.config.Config; import io.supertokens.cronjobs.Cronjobs; @@ -84,46 +85,52 @@ private TenantConfig[] getAllTenantsFromDb() throws StorageQueryException { return StorageLayer.getMultitenancyStorage(main).getAllTenants(); } - public void refreshTenantsInCoreIfRequired(boolean reloadAllResources) { + public List refreshTenantsInCoreBasedOnChangesInCoreConfigOrIfTenantListChanged(boolean reloadAllResources) { try { - main.getResourceDistributor().withResourceDistributorLock(() -> { + return main.getResourceDistributor().withResourceDistributorLock(() -> { try { TenantConfig[] tenantsFromDb = getAllTenantsFromDb(); - boolean hasChanged = false; - if (tenantsFromDb.length != tenantConfigs.length) { - hasChanged = true; - } else { - Map fromDb = new HashMap<>(); - for (TenantConfig t : tenantsFromDb) { - fromDb.put(t.tenantIdentifier, t); - } - for (TenantConfig t : this.tenantConfigs) { - TenantConfig fromDbConfig = fromDb.get(t.tenantIdentifier); - if (!t.deepEquals(fromDbConfig)) { - hasChanged = true; - break; - } + Map normalizedTenantsFromDb = Config.getNormalisedConfigsForAllTenants( + tenantsFromDb, Config.getBaseConfigAsJsonObject(main)); + + Map normalizedTenantsFromMemory = Config.getNormalisedConfigsForAllTenants( + this.tenantConfigs, Config.getBaseConfigAsJsonObject(main)); + + List tenantsThatChanged = new ArrayList<>(); + + for (Map.Entry entry : normalizedTenantsFromMemory.entrySet()) { + JsonObject tenantConfigFromMemory = entry.getValue(); + JsonObject tenantConfigFromDb = normalizedTenantsFromDb.get(entry.getKey()); + + if (!tenantConfigFromMemory.equals(tenantConfigFromDb)) { + tenantsThatChanged.add(entry.getKey().getTenantIdentifier()); } } + boolean sameNumberOfTenants = tenantsFromDb.length == this.tenantConfigs.length; + this.tenantConfigs = tenantsFromDb; - if (!hasChanged) { - return; + if (tenantsThatChanged.size() == 0 && sameNumberOfTenants) { + return tenantsThatChanged; } + ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB, null); + // this order is important. For example, storageLayer depends on config, and cronjobs depends on // storageLayer if (reloadAllResources) { - forceReloadAllResources(); + forceReloadAllResources(tenantsThatChanged); } else { // we do these two here cause they don't really depend on any table in the db, and these // two are required for allocating any further resource for this tenant - loadConfig(); + loadConfig(tenantsThatChanged); loadStorageLayer(); } + return tenantsThatChanged; } catch (Exception e) { Logging.error(main, TenantIdentifier.BASE_TENANT, e.getMessage(), false, e); + return new ArrayList<>(); } }); } catch (ResourceDistributor.FuncException e) { @@ -131,33 +138,34 @@ public void refreshTenantsInCoreIfRequired(boolean reloadAllResources) { } } - public void forceReloadAllResources() { + public void forceReloadAllResources(List tenantsThatChanged) { try { main.getResourceDistributor().withResourceDistributorLock(() -> { try { - loadConfig(); + loadConfig(tenantsThatChanged); loadStorageLayer(); - loadFeatureFlag(); - loadSigningKeys(); + loadFeatureFlag(tenantsThatChanged); + loadSigningKeys(tenantsThatChanged); refreshCronjobs(); } catch (Exception e) { Logging.error(main, TenantIdentifier.BASE_TENANT, e.getMessage(), false, e); } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new IllegalStateException(e); } } - public void loadConfig() throws IOException, InvalidConfigException { - Config.loadAllTenantConfig(main, this.tenantConfigs); + public void loadConfig(List tenantsThatChanged) throws IOException, InvalidConfigException { + Config.loadAllTenantConfig(main, this.tenantConfigs, tenantsThatChanged); } public void loadStorageLayer() throws IOException, InvalidConfigException { StorageLayer.loadAllTenantStorage(main, this.tenantConfigs); } - public void loadFeatureFlag() { + public void loadFeatureFlag(List tenantsThatChanged) { List apps = new ArrayList<>(); Set appsSet = new HashSet<>(); for (TenantConfig t : tenantConfigs) { @@ -167,10 +175,10 @@ public void loadFeatureFlag() { apps.add(t.tenantIdentifier.toAppIdentifier()); appsSet.add(t.tenantIdentifier.toAppIdentifier()); } - FeatureFlag.loadForAllTenants(main, apps); + FeatureFlag.loadForAllTenants(main, apps, tenantsThatChanged); } - public void loadSigningKeys() throws UnsupportedJWTSigningAlgorithmException { + public void loadSigningKeys(List tenantsThatChanged) throws UnsupportedJWTSigningAlgorithmException { List apps = new ArrayList<>(); Set appsSet = new HashSet<>(); for (TenantConfig t : tenantConfigs) { @@ -180,10 +188,10 @@ public void loadSigningKeys() throws UnsupportedJWTSigningAlgorithmException { apps.add(t.tenantIdentifier.toAppIdentifier()); appsSet.add(t.tenantIdentifier.toAppIdentifier()); } - AccessTokenSigningKey.loadForAllTenants(main, apps); - RefreshTokenKey.loadForAllTenants(main, apps); - JWTSigningKey.loadForAllTenants(main, apps); - SigningKeys.loadForAllTenants(main, apps); + AccessTokenSigningKey.loadForAllTenants(main, apps, tenantsThatChanged); + RefreshTokenKey.loadForAllTenants(main, apps, tenantsThatChanged); + JWTSigningKey.loadForAllTenants(main, apps, tenantsThatChanged); + SigningKeys.loadForAllTenants(main, apps, tenantsThatChanged); } private void refreshCronjobs() { @@ -193,7 +201,16 @@ private void refreshCronjobs() { public TenantConfig[] getAllTenants() { try { - return main.getResourceDistributor().withResourceDistributorLockWithReturn(() -> this.tenantConfigs); + return main.getResourceDistributor().withResourceDistributorLockWithReturn(() -> { + // Returning a deep copy of the tenantConfigs array so that the functions consuming it + // do not modify the original array + TenantConfig[] tenantConfigs = new TenantConfig[this.tenantConfigs.length]; + + for (int i = 0; i < this.tenantConfigs.length; i++) { + tenantConfigs[i] = new TenantConfig(this.tenantConfigs[i]); + } + return tenantConfigs; + }); } catch (ResourceDistributor.FuncException e) { throw new IllegalStateException(e); } diff --git a/src/main/java/io/supertokens/session/refreshToken/RefreshTokenKey.java b/src/main/java/io/supertokens/session/refreshToken/RefreshTokenKey.java index 5bf343dae..fb4fcc53f 100644 --- a/src/main/java/io/supertokens/session/refreshToken/RefreshTokenKey.java +++ b/src/main/java/io/supertokens/session/refreshToken/RefreshTokenKey.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.pluginInterface.session.noSqlStorage.SessionNoSQLStorage_1; @@ -72,7 +73,7 @@ public static RefreshTokenKey getInstance(Main main) { } } - public static void loadForAllTenants(Main main, List apps) { + public static void loadForAllTenants(Main main, List apps, List tenantsThatChanged) { try { main.getResourceDistributor().withResourceDistributorLock(() -> { Map existingResources = @@ -82,7 +83,7 @@ public static void loadForAllTenants(Main main, List apps) { for (AppIdentifier app : apps) { ResourceDistributor.SingletonResource resource = existingResources.get( new ResourceDistributor.KeyClass(app, RESOURCE_KEY)); - if (resource != null) { + if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) { main.getResourceDistributor().setResource(app, RESOURCE_KEY, resource); } else { @@ -95,6 +96,7 @@ public static void loadForAllTenants(Main main, List apps) { } } } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new RuntimeException(e); @@ -185,4 +187,4 @@ private String maybeGenerateNewKeyAndUpdateInDb() throw new QuitProgramException("Unsupported storage type detected"); } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/signingkeys/AccessTokenSigningKey.java b/src/main/java/io/supertokens/signingkeys/AccessTokenSigningKey.java index bbc679538..4f803e98a 100644 --- a/src/main/java/io/supertokens/signingkeys/AccessTokenSigningKey.java +++ b/src/main/java/io/supertokens/signingkeys/AccessTokenSigningKey.java @@ -34,6 +34,7 @@ import io.supertokens.pluginInterface.jwt.nosqlstorage.JWTRecipeNoSQLStorage_1; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.pluginInterface.session.noSqlStorage.SessionNoSQLStorage_1; @@ -89,7 +90,7 @@ public static AccessTokenSigningKey getInstance(Main main) { } } - public static void loadForAllTenants(Main main, List apps) { + public static void loadForAllTenants(Main main, List apps, List tenantsThatChanged) { try { main.getResourceDistributor().withResourceDistributorLock(() -> { Map existingResources = @@ -101,7 +102,7 @@ public static void loadForAllTenants(Main main, List apps) { new ResourceDistributor.KeyClass( app, RESOURCE_KEY)); - if (resource != null) { + if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) { main.getResourceDistributor() .setResource(app, RESOURCE_KEY, @@ -118,6 +119,7 @@ public static void loadForAllTenants(Main main, List apps) { } } } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new RuntimeException(e); @@ -363,4 +365,4 @@ public int getDynamicSigningKeyOverlapMS() throws TenantOrAppNotFoundException { return dynamicSigningKeyOverlapMS; } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/signingkeys/JWTSigningKey.java b/src/main/java/io/supertokens/signingkeys/JWTSigningKey.java index 4d2b6f490..89754933f 100644 --- a/src/main/java/io/supertokens/signingkeys/JWTSigningKey.java +++ b/src/main/java/io/supertokens/signingkeys/JWTSigningKey.java @@ -31,6 +31,7 @@ import io.supertokens.pluginInterface.jwt.nosqlstorage.JWTRecipeNoSQLStorage_1; import io.supertokens.pluginInterface.jwt.sqlstorage.JWTRecipeSQLStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; @@ -60,7 +61,7 @@ public static JWTSigningKey getInstance(Main main) { } } - public static void loadForAllTenants(Main main, List apps) + public static void loadForAllTenants(Main main, List apps, List tenantsThatChanged) throws UnsupportedJWTSigningAlgorithmException { try { main.getResourceDistributor().withResourceDistributorLock(() -> { @@ -72,7 +73,7 @@ public static void loadForAllTenants(Main main, List apps) for (AppIdentifier app : apps) { ResourceDistributor.SingletonResource resource = existingResources.get( new ResourceDistributor.KeyClass(app, RESOURCE_KEY)); - if (resource != null) { + if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) { main.getResourceDistributor().setResource(app, RESOURCE_KEY, resource); } else { @@ -91,6 +92,7 @@ public static void loadForAllTenants(Main main, List apps) } catch (UnsupportedJWTSigningAlgorithmException e) { throw new ResourceDistributor.FuncException(e); } + return null; }); } catch (ResourceDistributor.FuncException e) { if (e.getCause() instanceof UnsupportedJWTSigningAlgorithmException) { @@ -299,4 +301,4 @@ private JWTSigningKeyInfo generateKeyForAlgorithm(SupportedAlgorithms algorithm) throw new IllegalArgumentException(); } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/signingkeys/SigningKeys.java b/src/main/java/io/supertokens/signingkeys/SigningKeys.java index 7cb52576f..0dba3049a 100644 --- a/src/main/java/io/supertokens/signingkeys/SigningKeys.java +++ b/src/main/java/io/supertokens/signingkeys/SigningKeys.java @@ -28,6 +28,7 @@ import io.supertokens.pluginInterface.jwt.JWTAsymmetricSigningKeyInfo; import io.supertokens.pluginInterface.jwt.JWTSigningKeyInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.utils.Utils; import org.jetbrains.annotations.TestOnly; @@ -67,7 +68,7 @@ public static SigningKeys getInstance(Main main) { } } - public static void loadForAllTenants(Main main, List apps) { + public static void loadForAllTenants(Main main, List apps, List tenantsThatChanged) { try { main.getResourceDistributor().withResourceDistributorLock(() -> { Map existingResources = @@ -77,7 +78,7 @@ public static void loadForAllTenants(Main main, List apps) { for (AppIdentifier app : apps) { ResourceDistributor.SingletonResource resource = existingResources.get( new ResourceDistributor.KeyClass(app, RESOURCE_KEY)); - if (resource != null) { + if (resource != null && !tenantsThatChanged.contains(app.getAsPublicTenantIdentifier())) { main.getResourceDistributor().setResource(app, RESOURCE_KEY, resource); } else { @@ -86,6 +87,7 @@ public static void loadForAllTenants(Main main, List apps) { new SigningKeys(app, main)); } } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new RuntimeException(e); @@ -374,4 +376,4 @@ private static byte[] toBytesUnsigned(final BigInteger bigInt) { System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); return resizedBytes; } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/storageLayer/StorageLayer.java b/src/main/java/io/supertokens/storageLayer/StorageLayer.java index 4d52ae0fe..e305a09fe 100644 --- a/src/main/java/io/supertokens/storageLayer/StorageLayer.java +++ b/src/main/java/io/supertokens/storageLayer/StorageLayer.java @@ -187,6 +187,10 @@ public static void initPrimary(Main main, String pluginFolderPath, JsonObject co public static void loadAllTenantStorage(Main main, TenantConfig[] tenants) throws InvalidConfigException, IOException { + // We decided not to include tenantsThatChanged in this function because we do not want to reload the storage + // when the db config has not change. And when db config has changed, it results in a + // different userPoolId + connectionPoolId, which in turn results in a new storage instance + ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.LOADING_ALL_TENANT_STORAGE, null); Map normalisedConfigs = Config.getNormalisedConfigsForAllTenants( @@ -283,6 +287,7 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants) // we still want other tenants to continue to work } } + return null; }); } catch (ResourceDistributor.FuncException e) { throw new RuntimeException(e); diff --git a/src/test/java/io/supertokens/test/CronjobTest.java b/src/test/java/io/supertokens/test/CronjobTest.java index e45daee2f..25f9e719d 100644 --- a/src/test/java/io/supertokens/test/CronjobTest.java +++ b/src/test/java/io/supertokens/test/CronjobTest.java @@ -230,4 +230,23 @@ public void testNormalCronjob() throws Exception { } + + @Test + public void testAddingCronJobTwice() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + int initialSize = Cronjobs.getInstance(process.getProcess()).getTasks().size(); + + Cronjobs.addCronjob(process.getProcess(), NormalCronjob.getInstance(process.getProcess())); + assertEquals(initialSize + 1, Cronjobs.getInstance(process.getProcess()).getTasks().size()); + + Cronjobs.addCronjob(process.getProcess(), NormalCronjob.getInstance(process.getProcess())); + assertEquals(initialSize + 1, Cronjobs.getInstance(process.getProcess()).getTasks().size()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/PathRouterTest.java b/src/test/java/io/supertokens/test/PathRouterTest.java index cc6cddebb..20f85d728 100644 --- a/src/test/java/io/supertokens/test/PathRouterTest.java +++ b/src/test/java/io/supertokens/test/PathRouterTest.java @@ -49,6 +49,7 @@ import org.mockito.Mockito; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import static org.junit.Assert.*; @@ -1527,7 +1528,7 @@ public void tenantNotFoundTest3() new TenantConfig(new TenantIdentifier("localhost:3567", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { @@ -2725,7 +2726,7 @@ public void tenantNotFoundWithAppIdTest3() new TenantConfig(new TenantIdentifier("localhost:3567", "app1", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { diff --git a/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java b/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java index 813f2ecc6..4bbb630d5 100644 --- a/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java @@ -56,7 +56,8 @@ public void checkingCronJob() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); CronTaskTest.getInstance(process.getProcess()) - .setIntervalInSeconds(DeleteExpiredPasswordResetTokens.RESOURCE_KEY, 2); + .setIntervalInSeconds(DeleteExpiredPasswordResetTokens.RESOURCE_KEY, 1); + Utils.setValueInConfig("password_reset_token_lifetime", "4000"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { @@ -68,16 +69,15 @@ public void checkingCronJob() throws Exception { String tok = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); String tok2 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - io.supertokens.emailpassword.EmailPasswordTest.getInstance(process.getProcess()) - .setPasswordResetTokenLifetime(10); + Thread.sleep(2000); - EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String tok3 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String tok4 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id).length == 4); - Thread.sleep(3000); + Thread.sleep(3500); PasswordResetTokenInfo[] tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id); @@ -85,10 +85,10 @@ public void checkingCronJob() throws Exception { assert (tokens.length == 2); assert (!tokens[0].token.equals(tokens[1].token)); - assert (tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok)) - || tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok2))); - assert (tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok)) - || tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok2))); + assert (tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok3)) + || tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok4))); + assert (tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok3)) + || tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok4))); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java index 3422d36e5..c1346d096 100644 --- a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java @@ -245,8 +245,7 @@ public void passwordResetTokenExpiredCheck() throws Exception { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - io.supertokens.emailpassword.EmailPasswordTest.getInstance(process.getProcess()) - .setPasswordResetTokenLifetime(10); + Utils.setValueInConfig("password_reset_token_lifetime", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); diff --git a/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java b/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java index 1908988fd..78beca854 100644 --- a/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java +++ b/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java @@ -57,8 +57,9 @@ public void checkingCronJob() throws Exception { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("email_verification_token_lifetime", "4000"); CronTaskTest.getInstance(process.getProcess()) - .setIntervalInSeconds(DeleteExpiredEmailVerificationTokens.RESOURCE_KEY, 2); + .setIntervalInSeconds(DeleteExpiredEmailVerificationTokens.RESOURCE_KEY, 1); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { @@ -70,17 +71,16 @@ public void checkingCronJob() throws Exception { String tok = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); String tok2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - io.supertokens.emailverification.EmailVerificationTest.getInstance(process.getProcess()) - .setEmailVerificationTokenLifetime(10); + Thread.sleep(2000); - EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String tok3 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String tok4 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); assert (((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.id, user.email).length == 4); - Thread.sleep(3000); + Thread.sleep(3500); EmailVerificationTokenInfo[] tokens = ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.id, user.email); @@ -88,10 +88,10 @@ public void checkingCronJob() throws Exception { assert (tokens.length == 2); assert (!tokens[0].token.equals(tokens[1].token)); - assert (tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok)) - || tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok2))); - assert (tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok)) - || tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok2))); + assert (tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok3)) + || tokens[0].token.equals(io.supertokens.utils.Utils.hashSHA256(tok4))); + assert (tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok3)) + || tokens[1].token.equals(io.supertokens.utils.Utils.hashSHA256(tok4))); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java b/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java index 2d760e107..de78a89cb 100644 --- a/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java +++ b/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java @@ -185,9 +185,7 @@ public void useAnExpiredTokenItShouldThrowAnError() throws Exception { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - - io.supertokens.emailverification.EmailVerificationTest.getInstance(process.getProcess()) - .setEmailVerificationTokenLifetime(10); + Utils.setValueInConfig("email_verification_token_lifetime", "10"); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); diff --git a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java index 47bbd1bb2..3cac1511b 100644 --- a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java +++ b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java @@ -24,8 +24,8 @@ import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; import io.supertokens.config.CoreConfigTestContent; -import io.supertokens.exceptions.QuitProgramException; import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; @@ -37,6 +37,10 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.session.refreshToken.RefreshTokenKey; +import io.supertokens.signingkeys.AccessTokenSigningKey; +import io.supertokens.signingkeys.JWTSigningKey; +import io.supertokens.signingkeys.SigningKeys; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -47,7 +51,7 @@ import java.io.File; import java.io.IOException; -import java.util.List; +import java.util.ArrayList; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; @@ -141,7 +145,7 @@ public void mergingTenantWithBaseConfigWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); Assert.assertEquals(Config.getConfig(process.getProcess()).getRefreshTokenValidity(), (long) 144001 * 60 * 1000); @@ -193,7 +197,7 @@ public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); fail(); } catch (InvalidConfigException e) { assert (e.getMessage() @@ -225,7 +229,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() new TenantConfig(new TenantIdentifier(null, null, "abc"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); fail(); } catch (InvalidConfigException e) { assert (e.getMessage() @@ -267,7 +271,7 @@ public void mergingDifferentUserPoolTenantWithBaseConfigWithConflictingConfigsSh new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}); + tenantConfig)}, new ArrayList<>()); } @@ -340,7 +344,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenantConfig); } - Config.loadAllTenantConfig(process.getProcess(), tenants); + Config.loadAllTenantConfig(process.getProcess(), tenants, new ArrayList<>()); Assert.assertEquals(Config.getConfig(new TenantIdentifier(null, null, null), process.getProcess()) .getEmailVerificationTokenLifetime(), @@ -410,7 +414,7 @@ public void testMappingSameUserPoolToDifferentConnectionURIThrowsError() } try { - Config.loadAllTenantConfig(process.getProcess(), tenants); + Config.loadAllTenantConfig(process.getProcess(), tenants, new ArrayList<>()); fail(); } catch (InvalidConfigException e) { assert (e.getMessage() @@ -1223,4 +1227,379 @@ public void testInvalidConfigWhileCreatingNewTenant() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testThatConfigChangesReloadsConfig() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); + + { + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + } + + { + Config configBefore = Config.getInstance(t1, process.getProcess()); + + ProcessState.getInstance(process.getProcess()).clear(); + + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); + + Config configAfter = Config.getInstance(t1, process.getProcess()); + + assertEquals(configBefore, configAfter); + } + + { + Config configBefore = Config.getInstance(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + coreConfig.addProperty("email_verification_token_lifetime", 2000); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + Config configAfter = Config.getInstance(t1, process.getProcess()); + + assertNotEquals(configBefore, configAfter); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatConfigChangesInAppReloadsConfigInTenant() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier a1 = new TenantIdentifier(null, "a1", null); + TenantIdentifier t1 = new TenantIdentifier(null, "a1", "t1"); + + { + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + a1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + } + + { + ProcessState.getInstance(process.getProcess()).clear(); + + Config configBefore = Config.getInstance(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + a1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false);Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); + + Config configAfter = Config.getInstance(t1, process.getProcess()); + + assertEquals(configBefore, configAfter); + } + + { + Config configBefore = Config.getInstance(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + coreConfig.addProperty("email_verification_token_lifetime", 2000); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + a1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + Config configAfter = Config.getInstance(t1, process.getProcess()); + + assertNotEquals(configBefore, configAfter); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatConfigChangesReloadsStorageLayer() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier t1 = new TenantIdentifier(null, null, "t1"); + + { + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + } + + { + ProcessState.getInstance(process.getProcess()).clear(); + Storage storageLayerBefore = StorageLayer.getStorage(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); + Storage storageLayerAfter = StorageLayer.getStorage(t1, process.getProcess()); + + assertEquals(storageLayerBefore, storageLayerAfter); + } + + { + Storage storageLayerBefore = StorageLayer.getStorage(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + coreConfig.addProperty("email_verification_token_lifetime", 2000); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + Storage storageLayerAfter = StorageLayer.getStorage(t1, process.getProcess()); + + assertEquals(storageLayerBefore, storageLayerAfter); + } + + { + Storage storageLayerBefore = StorageLayer.getStorage(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + Storage storageLayerAfter = StorageLayer.getStorage(t1, process.getProcess()); + + assertNotEquals(storageLayerBefore, storageLayerAfter); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatConfigChangesReloadsFeatureFlag() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + AppIdentifier t1 = new AppIdentifier(null, "a1"); + + { + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + } + + { + ProcessState.getInstance(process.getProcess()).clear(); + + FeatureFlag featureFlagBefore = FeatureFlag.getInstance(process.getProcess(), t1); + + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); + FeatureFlag featureFlagAfter = FeatureFlag.getInstance(process.getProcess(), t1); + + assertEquals(featureFlagBefore, featureFlagAfter); + } + + { + FeatureFlag featureFlagBefore = FeatureFlag.getInstance(process.getProcess(), t1); + + JsonObject coreConfig = new JsonObject(); + coreConfig.addProperty("email_verification_token_lifetime", 2000); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + FeatureFlag featureFlagAfter = FeatureFlag.getInstance(process.getProcess(), t1); + + assertNotEquals(featureFlagBefore, featureFlagAfter); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatConfigChangesReloadsSigningKeys() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + AppIdentifier t1 = new AppIdentifier(null, "a1"); + + { + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + } + + { + ProcessState.getInstance(process.getProcess()).clear(); + + AccessTokenSigningKey accessTokenSigningKeyBefore = AccessTokenSigningKey.getInstance(t1, process.getProcess()); + RefreshTokenKey refreshTokenKeyBefore = RefreshTokenKey.getInstance(t1, process.getProcess()); + JWTSigningKey jwtSigningKeyBefore = JWTSigningKey.getInstance(t1, process.getProcess()); + SigningKeys signingKeysBefore = SigningKeys.getInstance(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); + + AccessTokenSigningKey accessTokenSigningKeyAfter = AccessTokenSigningKey.getInstance(t1, process.getProcess()); + RefreshTokenKey refreshTokenKeyAfter = RefreshTokenKey.getInstance(t1, process.getProcess()); + JWTSigningKey jwtSigningKeyAfter = JWTSigningKey.getInstance(t1, process.getProcess()); + SigningKeys signingKeysAfter = SigningKeys.getInstance(t1, process.getProcess()); + + assertEquals(accessTokenSigningKeyBefore, accessTokenSigningKeyAfter); + assertEquals(refreshTokenKeyBefore, refreshTokenKeyAfter); + assertEquals(jwtSigningKeyBefore, jwtSigningKeyAfter); + assertEquals(signingKeysBefore, signingKeysAfter); + } + + { + AccessTokenSigningKey accessTokenSigningKeyBefore = AccessTokenSigningKey.getInstance(t1, process.getProcess()); + RefreshTokenKey refreshTokenKeyBefore = RefreshTokenKey.getInstance(t1, process.getProcess()); + JWTSigningKey jwtSigningKeyBefore = JWTSigningKey.getInstance(t1, process.getProcess()); + SigningKeys signingKeysBefore = SigningKeys.getInstance(t1, process.getProcess()); + + JsonObject coreConfig = new JsonObject(); + coreConfig.addProperty("email_verification_token_lifetime", 2000); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + t1.getAsPublicTenantIdentifier(), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ), false); + + AccessTokenSigningKey accessTokenSigningKeyAfter = AccessTokenSigningKey.getInstance(t1, process.getProcess()); + RefreshTokenKey refreshTokenKeyAfter = RefreshTokenKey.getInstance(t1, process.getProcess()); + JWTSigningKey jwtSigningKeyAfter = JWTSigningKey.getInstance(t1, process.getProcess()); + SigningKeys signingKeysAfter = SigningKeys.getInstance(t1, process.getProcess()); + + assertNotEquals(accessTokenSigningKeyBefore, accessTokenSigningKeyAfter); + assertNotEquals(refreshTokenKeyBefore, refreshTokenKeyAfter); + assertNotEquals(jwtSigningKeyBefore, jwtSigningKeyAfter); + assertNotEquals(signingKeysBefore, signingKeysAfter); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } diff --git a/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java b/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java index 4ce4ab06d..d8d866e64 100644 --- a/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java +++ b/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java @@ -121,7 +121,7 @@ public void keysAreGeneratedForAllUserPoolIds() apps.add(t.tenantIdentifier.toAppIdentifier()); } apps.add(new AppIdentifier(null, null)); // Add base app - AccessTokenSigningKey.loadForAllTenants(process.getProcess(), apps); + AccessTokenSigningKey.loadForAllTenants(process.getProcess(), apps, new ArrayList<>()); assertEquals( SigningKeys.getInstance(new AppIdentifier(null, null), process.main).getDynamicKeys() @@ -188,7 +188,7 @@ public void signingKeyClassesAreThereForAllTenants() apps.add(t.tenantIdentifier.toAppIdentifier()); } apps.add(new AppIdentifier(null, null)); // Add base app - AccessTokenSigningKey.loadForAllTenants(process.getProcess(), apps); + AccessTokenSigningKey.loadForAllTenants(process.getProcess(), apps, new ArrayList<>()); assertEquals( SigningKeys.getInstance(new AppIdentifier(null, null), process.main).getDynamicKeys() diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java index 07dd65a78..af75d1f6c 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java @@ -48,7 +48,7 @@ public class MultitenantAPITest { TestingProcessManager.TestingProcess process; - TenantIdentifier t1, t2, t3; + TenantIdentifier t1, t2, t3, t4; @AfterClass public static void afterTesting() { @@ -83,7 +83,7 @@ private void createTenants() throws StorageQueryException, TenantOrAppNotFoundException, InvalidProviderConfigException, FeatureNotEnabledException, IOException, InvalidConfigException, CannotModifyBaseConfigException, BadPermissionException { - // User pool 1 - (null, a1, null) + // User pool 1 - (null, a1, null), (null, a2, null) // User pool 2 - (null, a1, t1), (null, a1, t2) { // tenant 1 @@ -146,9 +146,30 @@ private void createTenants() ); } + { // tenant 4 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a2", null); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(false, null), + new PasswordlessConfig(false), + config + ) + ); + } + t1 = new TenantIdentifier(null, "a1", null); t2 = new TenantIdentifier(null, "a1", "t1"); t3 = new TenantIdentifier(null, "a1", "t2"); + t4 = new TenantIdentifier(null, "a2", null); } private JsonObject emailPasswordSignUp(TenantIdentifier tenantIdentifier, String email, String password) @@ -379,4 +400,29 @@ public void testSameExternalIdAcrossUserPoolPrioritizesTenantOfInterest() throws assertEquals(user2.get("id").getAsString(), mapping.get("superTokensUserId").getAsString()); } } + + @Test + public void testUserIdFromDifferentAppIsAllowedForUserIdMapping() throws Exception { + JsonObject user1 = emailPasswordSignUp(t1, "user@example.com", "password1"); + JsonObject user2 = emailPasswordSignUp(t4, "user1@example.com", "password2"); + JsonObject user3 = emailPasswordSignUp(t4, "user2@example.com", "password3"); + + successfulCreateUserIdMapping(t4, user2.get("id").getAsString(), user1.get("id").getAsString()); + successfulCreateUserIdMapping(t1, user1.get("id").getAsString(), user2.get("id").getAsString()); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("superTokensUserId", user3.get("id").getAsString()); + requestBody.addProperty("externalUserId", user2.get("id").getAsString()); + + try { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(t4, "/recipe/userid/map"), requestBody, + 1000, 1000, null, + SemVer.v2_22.get(), "useridmapping"); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Cannot create a userId mapping where the externalId is also a SuperTokens userID", e.getMessage()); + } + } }