diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index 79a946d84..d14c38e6b 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -192,6 +193,16 @@ public static void deleteUser(Main main, String userId) deleteUser(appIdentifier, userId, mapping); } + @TestOnly + public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + throws StorageQueryException, StorageTransactionLogicException { + Storage storage = appIdentifierWithStorage.getStorage(); + UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifierWithStorage, + userId, UserIdType.ANY); + + deleteUser(appIdentifierWithStorage, userId, mapping); + } + private static void deleteNonAuthRecipeUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId) throws StorageQueryException, StorageTransactionLogicException { diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index e45ccd3a6..61a7efb1f 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -122,7 +122,8 @@ private static void validateConfigJsonForInvalidKeys(Main main, JsonObject coreC } } - private static void validateTenantConfig(Main main, TenantConfig targetTenantConfig, boolean shouldPreventDbConfigUpdate) + private static void validateTenantConfig(Main main, TenantConfig targetTenantConfig, boolean shouldPreventDbConfigUpdate, + boolean skipThirdPartyConfigValidation) throws IOException, InvalidConfigException, InvalidProviderConfigException, BadPermissionException, TenantOrAppNotFoundException, CannotModifyBaseConfigException { @@ -172,7 +173,7 @@ private static void validateTenantConfig(Main main, TenantConfig targetTenantCon } // validate third party config - { + if (!skipThirdPartyConfigValidation) { ThirdParty.verifyThirdPartyProvidersArray(targetTenantConfig.thirdPartyConfig.providers); } } @@ -190,6 +191,13 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan throws CannotModifyBaseConfigException, BadPermissionException, StorageQueryException, FeatureNotEnabledException, IOException, InvalidConfigException, InvalidProviderConfigException, TenantOrAppNotFoundException { + return addNewOrUpdateAppOrTenant(main, newTenant, shouldPreventDbConfigUpdate, false); + } + + public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenant, boolean shouldPreventDbConfigUpdate, boolean skipThirdPartyConfigValidation) + throws CannotModifyBaseConfigException, BadPermissionException, + StorageQueryException, FeatureNotEnabledException, IOException, InvalidConfigException, + InvalidProviderConfigException, TenantOrAppNotFoundException { if (StorageLayer.getBaseStorage(main).getType() != STORAGE_TYPE.SQL) { if (newTenant.tenantIdentifier.equals(TenantIdentifier.BASE_TENANT)) { @@ -204,7 +212,7 @@ public static boolean addNewOrUpdateAppOrTenant(Main main, TenantConfig newTenan // a big issue at the moment, but we want to solve this by taking appropriate database locks on // connectionuridomain, appid and tenantid. - validateTenantConfig(main, newTenant, shouldPreventDbConfigUpdate); + validateTenantConfig(main, newTenant, shouldPreventDbConfigUpdate, skipThirdPartyConfigValidation); boolean creationInSharedDbSucceeded = false; List tenantsThatChanged = new ArrayList<>(); diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java index c1332a826..b437699cc 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java @@ -61,6 +61,11 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO String thirdPartyId = InputParser.parseStringOrThrowError(input, "thirdPartyId", false); thirdPartyId = thirdPartyId.trim(); + Boolean skipValidation = InputParser.parseBooleanOrThrowError(input, "skipValidation", true); + if (skipValidation == null) { + skipValidation = false; + } + try { TenantIdentifierWithStorage tenantIdentifier = this.getTenantIdentifierWithStorageFromRequest(req); @@ -106,7 +111,7 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO config.passwordlessConfig, config.coreConfig); - Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectDbConfig(req)); + Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectDbConfig(req), skipValidation); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); diff --git a/src/test/java/io/supertokens/test/CronjobTest.java b/src/test/java/io/supertokens/test/CronjobTest.java index 537a64ae5..56a6bab0c 100644 --- a/src/test/java/io/supertokens/test/CronjobTest.java +++ b/src/test/java/io/supertokens/test/CronjobTest.java @@ -16,14 +16,19 @@ package io.supertokens.test; +import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.cronjobs.CronTask; import io.supertokens.cronjobs.Cronjobs; import io.supertokens.exceptions.QuitProgramException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.Storage; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.storageLayer.StorageLayer; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -250,4 +255,86 @@ public void testAddingCronJobTwice() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testAddingTenantsDoesNotIncreaseCronJobs() 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)); + + int initialSize = Cronjobs.getInstance(process.getProcess()).getTasks().size(); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a2", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a2", "t2"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + assertEquals(initialSize, Cronjobs.getInstance(process.getProcess()).getTasks().size()); + + JsonObject config = new JsonObject(); + StorageLayer.getStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a3", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a3", "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a4", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a4", "t2"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + + assertEquals(initialSize, Cronjobs.getInstance(process.getProcess()).getTasks().size()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/HelloAPITest.java b/src/test/java/io/supertokens/test/HelloAPITest.java new file mode 100644 index 000000000..bc7d7f418 --- /dev/null +++ b/src/test/java/io/supertokens/test/HelloAPITest.java @@ -0,0 +1,168 @@ +/* + * 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; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class HelloAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testHelloAPIWithBasePath1() 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}); + Utils.setValueInConfig("base_path", "/base"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/base", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/base/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testHelloAPIWithBasePath2() 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}); + Utils.setValueInConfig("base_path", "/hello"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testHelloAPIWithBasePath3() 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}); + Utils.setValueInConfig("base_path", "/hello"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "hello", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "hello", "hello"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "hello"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + String res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello/hello/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello/appid-hello/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + res = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/hello/appid-hello/hello/hello", null, 1000, 1000, + null, Utils.getCdiVersionStringLatestForTests(), ""); + assertEquals("Hello", res); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/ResourceDistributorTest.java b/src/test/java/io/supertokens/test/ResourceDistributorTest.java new file mode 100644 index 000000000..6fa860a3a --- /dev/null +++ b/src/test/java/io/supertokens/test/ResourceDistributorTest.java @@ -0,0 +1,107 @@ +/* + * 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; + +import io.supertokens.ProcessState; +import io.supertokens.ResourceDistributor; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class ResourceDistributorTest { + + private static class ResourceA extends ResourceDistributor.SingletonResource { + private static final String RESOURCE_ID = "io.supertokens.test.ResourceDistributorTest.ResourceA"; + + public ResourceA() { + } + } + + private static class ResourceB extends ResourceDistributor.SingletonResource { + private static final String RESOURCE_ID = "io.supertokens.test.ResourceDistributorTest.ResourceB"; + + public ResourceB() { + } + } + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testClearAllResourcesWithKeyWorksCorrectly() 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 a1 = new AppIdentifier(null, "a1"); + TenantIdentifier t1 = new TenantIdentifier(null, "a1", "t1"); + + process.getProcess().getResourceDistributor().setResource(a1, ResourceA.RESOURCE_ID, new ResourceA()); + process.getProcess().getResourceDistributor().setResource(t1, ResourceA.RESOURCE_ID, new ResourceA()); + + process.getProcess().getResourceDistributor().setResource(a1, ResourceB.RESOURCE_ID, new ResourceB()); + process.getProcess().getResourceDistributor().setResource(t1, ResourceB.RESOURCE_ID, new ResourceB()); + + assertTrue(process.getProcess().getResourceDistributor().getResource(a1, ResourceA.RESOURCE_ID) instanceof ResourceA); + assertTrue(process.getProcess().getResourceDistributor().getResource(t1, ResourceA.RESOURCE_ID) instanceof ResourceA); + + process.getProcess().getResourceDistributor().clearAllResourcesWithResourceKey(ResourceA.RESOURCE_ID); + + try { + process.getProcess().getResourceDistributor().getResource(a1, ResourceA.RESOURCE_ID); + fail(); + } catch (TenantOrAppNotFoundException e) { + // ignored + } + try { + process.getProcess().getResourceDistributor().getResource(t1, ResourceA.RESOURCE_ID); + fail(); + } catch (TenantOrAppNotFoundException e) { + // ignored + } + + assertTrue(process.getProcess().getResourceDistributor().getResource(a1, ResourceB.RESOURCE_ID) instanceof ResourceB); + assertTrue(process.getProcess().getResourceDistributor().getResource(t1, ResourceB.RESOURCE_ID) instanceof ResourceB); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } +} diff --git a/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java new file mode 100644 index 000000000..c6993c4fd --- /dev/null +++ b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java @@ -0,0 +1,301 @@ +/* + * 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.multitenant; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.ActiveUsersStorage; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.useridmapping.UserIdMapping; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.reflections.Reflections; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class AppTenantUserTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testDeletingAppDeleteNonAuthRecipeData() 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, EE_FEATURES.TOTP}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + // this list contains the package names for recipes which dont use UserIdMapping + ArrayList classesToSkip = new ArrayList<>( + List.of("io.supertokens.pluginInterface.jwt.JWTRecipeStorage", ActiveUsersStorage.class.getName())); + + Reflections reflections = new Reflections("io.supertokens.pluginInterface"); + Set> classes = reflections.getSubTypesOf(NonAuthRecipeStorage.class); + List names = classes.stream().map(Class::getCanonicalName).collect(Collectors.toList()); + List classNames = new ArrayList<>(); + for (String name : names) { + if (!name.contains("SQLStorage")) { + classNames.add(name); + } + } + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + + + for (TenantIdentifier t : new TenantIdentifier[]{app, tenant}) { + + for (String className : classNames) { + if (classesToSkip.contains(className)) { + continue; + } + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + TenantIdentifierWithStorage tWithStorage = t.withStorage(StorageLayer.getStorage(t, process.getProcess())); + + + UserInfo user = EmailPassword.signUp(tWithStorage, process.getProcess(), "test@example.com", "password"); + String userId = user.id; + + // create entry in nonAuth table + StorageLayer.getStorage(process.main).addInfoToNonAuthRecipesBasedOnUserId(app, className, userId); + + try { + UserIdMapping.assertThatUserIdIsNotBeingUsedInNonAuthRecipes( + tWithStorage.toAppIdentifierWithStorage(), userId); + fail(className); + } catch (Exception ignored) { + assertTrue(ignored.getMessage().contains("UserId is already in use")); + } + + // Delete app/tenant + Multitenancy.deleteTenant(tenant, process.getProcess()); + Multitenancy.deleteApp(app.toAppIdentifier(), process.getProcess()); + + // Create tenants again + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + UserIdMapping.assertThatUserIdIsNotBeingUsedInNonAuthRecipes(tWithStorage.toAppIdentifierWithStorage(), userId); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDisassociationOfUserDeletesNonAuthRecipeData() 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, EE_FEATURES.TOTP}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + // this list contains the package names for recipes which dont use UserIdMapping + ArrayList classesToSkip = new ArrayList<>( + List.of("io.supertokens.pluginInterface.jwt.JWTRecipeStorage", ActiveUsersStorage.class.getName())); + + Reflections reflections = new Reflections("io.supertokens.pluginInterface"); + Set> classes = reflections.getSubTypesOf(NonAuthRecipeStorage.class); + List names = classes.stream().map(Class::getCanonicalName).collect(Collectors.toList()); + List classNames = new ArrayList<>(); + for (String name : names) { + if (!name.contains("SQLStorage")) { + classNames.add(name); + } + } + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + TenantIdentifierWithStorage appWithStorage = app.withStorage( + StorageLayer.getStorage(app, process.getProcess())); + TenantIdentifierWithStorage tenantWithStorage = tenant.withStorage( + StorageLayer.getStorage(tenant, process.getProcess())); + + for (String className : classNames) { + if (classesToSkip.contains(className)) { + continue; + } + + UserInfo user = EmailPassword.signUp(appWithStorage, process.getProcess(), "test@example.com", "password"); + String userId = user.id; + + Multitenancy.addUserIdToTenant(process.getProcess(), tenantWithStorage, userId); + + // create entry in nonAuth table + tenantWithStorage.getStorage().addInfoToNonAuthRecipesBasedOnUserId(tenant, className, userId); + + try { + UserIdMapping.assertThatUserIdIsNotBeingUsedInNonAuthRecipes( + tenantWithStorage.toAppIdentifierWithStorage(), userId); + fail(className); + } catch (Exception ignored) { + assertTrue(ignored.getMessage().contains("UserId is already in use")); + } + + // Disassociate user + Multitenancy.removeUserIdFromTenant(process.getProcess(), tenantWithStorage, userId); + + assertFalse(AuthRecipe.deleteNonAuthRecipeUser(tenantWithStorage, userId)); // Nothing deleted indicates that the non auth recipe user data was deleted already + + AuthRecipe.deleteUser(appWithStorage.toAppIdentifierWithStorage(), userId); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deletingTenantKeepsTheUserInTheApp() 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, EE_FEATURES.TOTP}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + TenantIdentifier app = new TenantIdentifier(null, "a1", null); + TenantIdentifier tenant = new TenantIdentifier(null, "a1", "t1"); + + // Create tenants + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + app, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + tenant, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + TenantIdentifierWithStorage appWithStorage = app.withStorage( + StorageLayer.getStorage(app, process.getProcess())); + TenantIdentifierWithStorage tenantWithStorage = tenant.withStorage( + StorageLayer.getStorage(tenant, process.getProcess())); + + UserInfo user = EmailPassword.signUp(tenantWithStorage, process.getProcess(), "test@example.com", "password"); + String userId = user.id; + + Multitenancy.deleteTenant(tenant, process.getProcess()); + + Multitenancy.addUserIdToTenant(process.getProcess(), appWithStorage, userId); // user id must be intact to do this + + UserInfo appUser = EmailPassword.getUserUsingId(appWithStorage.toAppIdentifierWithStorage(), userId); + + assertNotNull(appUser); + assertEquals(userId, appUser.id); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java index bd0566964..05f28139a 100644 --- a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java +++ b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java @@ -29,6 +29,7 @@ import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.MultitenancyHelper; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; import io.supertokens.pluginInterface.STORAGE_TYPE; @@ -1706,4 +1707,178 @@ public void testThatConfigChangesReloadsSigningKeys() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testLoadAllTenantConfigWithDifferentConfigSavedInTheDb() throws Exception { + // What is saved in db is not overwritten + // New apps/tenants are added to the loaded config + + 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)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + // Save in db + JsonObject config = new JsonObject(); + config.addProperty("email_verification_token_lifetime", 100); + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + + // Now load a new set of configs + JsonObject config1 = new JsonObject(); + config1.addProperty("email_verification_token_lifetime", 200); + JsonObject config2 = new JsonObject(); + config2.addProperty("email_verification_token_lifetime", 300); + JsonObject config3 = new JsonObject(); + config3.addProperty("email_verification_token_lifetime", 400); + JsonObject config4 = new JsonObject(); + config4.addProperty("email_verification_token_lifetime", 500); + + TenantConfig[] tenantConfigs = new TenantConfig[]{ + new TenantConfig( + new TenantIdentifier(null, null, null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(false, null), + new PasswordlessConfig(true), + config1 + ), + new TenantConfig( + new TenantIdentifier(null, "a2", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(false, null), + new PasswordlessConfig(true), + config2 + ), + new TenantConfig( + new TenantIdentifier(null, "a2", "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config3 + ), + new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(false), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config4 + ), + }; + Config.loadAllTenantConfig(process.getProcess(), tenantConfigs); + + assertEquals( + 300, + Config.getConfig(new TenantIdentifier(null, "a2", null), process.getProcess()).getEmailVerificationTokenLifetime() + ); + assertEquals( + 400, + Config.getConfig(new TenantIdentifier(null, "a2", "t1"), process.getProcess()).getEmailVerificationTokenLifetime() + ); + assertEquals( + 100, + Config.getConfig(new TenantIdentifier(null, "a1", null), process.getProcess()).getEmailVerificationTokenLifetime() + ); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatMistypedConfigThrowsError() throws Exception { + String[] args = {"../"}; + + Utils.setValueInConfig("email_verification_token_lifetime", "144001"); + 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)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject mistypedConfig = new JsonObject(); + mistypedConfig.addProperty("foo", "bar"); + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + mistypedConfig + ), false); + fail(); + } catch (InvalidConfigException e) { + assertTrue(e.getMessage().contains("Invalid config key: foo")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testCoreSpecificConfigIsNotAllowedForNewTenants() 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)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String[] disallowedConfigs = new String[]{ + "port", + "host", + "info_log_path", + "error_log_path", + "max_server_pool_size", + "base_path", + "argon2_hashing_pool_size", + "log_level", + "firebase_password_hashing_pool_size", + "supertokens_saas_secret", + "supertokens_default_cdi_version" + }; + + for (String disallowedConfig : disallowedConfigs) { + JsonObject config = new JsonObject(); + if (disallowedConfig.contains("size") || disallowedConfig.contains("port")) { + config.addProperty(disallowedConfig, 1000); + } else { + config.addProperty(disallowedConfig, "somevalue"); + } + + try { + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + fail(); + } catch (InvalidConfigException e) { + assertTrue(e.getMessage().contains(disallowedConfig)); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java index 13007b1c3..4121ab3c3 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java @@ -252,4 +252,43 @@ public void testDeleteAppWorks() throws Exception { process.getProcess()); assertFalse(response.get("didExist").getAsBoolean()); } + + @Test + public void testAddingWithDifferentConnectionURIAddsToNullConnectionURI() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier("localhost:3567", null, null), + "a1", true, true, true, + new JsonObject()); + + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier("127.0.0.1:3567", null, null), + "a2", true, true, true, + new JsonObject()); + + JsonObject result = TestMultitenancyAPIHelper.listApps(new TenantIdentifier(null, null, null), process.getProcess()); + assertTrue(result.has("apps")); + assertEquals(3, result.get("apps").getAsJsonArray().size()); + + boolean foundA1 = false; + boolean foundA2 = false; + + for (JsonElement app : result.get("apps").getAsJsonArray()) { + JsonObject appObj = app.getAsJsonObject(); + + if (appObj.get("appId").getAsString().equals("a1")) { + foundA1 = true; + } else if (appObj.get("appId").getAsString().equals("a2")) { + foundA2 = true; + } + } + + assertTrue(foundA1); + assertTrue(foundA2); + } } 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 be2ae56a6..2aaaae3e9 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java @@ -205,8 +205,14 @@ public static JsonObject disassociateUserFromTenant(TenantIdentifier tenantIdent public static JsonObject addOrUpdateThirdPartyProviderConfig(TenantIdentifier tenantIdentifier, ThirdPartyConfig.Provider provider, Main main) throws HttpResponseException, IOException { + return addOrUpdateThirdPartyProviderConfig(tenantIdentifier, provider, false, main); + } + + public static JsonObject addOrUpdateThirdPartyProviderConfig(TenantIdentifier tenantIdentifier, ThirdPartyConfig.Provider provider, boolean skipValidation, Main main) + throws HttpResponseException, IOException { Gson gson = new Gson(); JsonObject requestBody = gson.toJsonTree(provider).getAsJsonObject(); + requestBody.addProperty("skipValidation", skipValidation); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/multitenancy/config/thirdparty"), @@ -315,4 +321,17 @@ public static JsonObject getEpUserById(TenantIdentifier tenantIdentifier, String assertEquals("OK", userResponse.getAsJsonPrimitive("status").getAsString()); return userResponse.getAsJsonObject("user"); } + + public static void createUserIdMapping(TenantIdentifier tenantIdentifier, String supertokensUserId, String externalUserId, Main main) + throws HttpResponseException, IOException { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("superTokensUserId", supertokensUserId); + requestBody.addProperty("externalUserId", externalUserId); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/userid/map"), requestBody, + 1000, 1000, null, + SemVer.v3_0.get(), "useridmapping"); + assertEquals("OK", response.get("status").getAsString()); + } } diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java b/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java new file mode 100644 index 000000000..c3f0b3634 --- /dev/null +++ b/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java @@ -0,0 +1,94 @@ +/* + * 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.multitenant.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpResponseException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class TestSkipValidationInCreateThirdParty { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testSkipValidation() 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)); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new JsonObject() + ), false); + + try { + TestMultitenancyAPIHelper.addOrUpdateThirdPartyProviderConfig(new TenantIdentifier(null, "a1", null), + new ThirdPartyConfig.Provider( + "boxy-saml", "Boxy SAML", new ThirdPartyConfig.ProviderClient[]{ + new ThirdPartyConfig.ProviderClient("web", "clientid", "clientsecret", null, null, null) + }, null, null, null, + null, null, null, null, null, null, null, + null + ), process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertTrue(e.getMessage().contains("a non empty string value must be specified for boxyURL in the additionalConfig for Boxy SAML provider")); + } + + TestMultitenancyAPIHelper.addOrUpdateThirdPartyProviderConfig(new TenantIdentifier(null, "a1", null), + new ThirdPartyConfig.Provider( + "boxy-saml", "Boxy SAML", new ThirdPartyConfig.ProviderClient[]{ + new ThirdPartyConfig.ProviderClient("web", "clientid", "clientsecret", null, null, null) + }, null, null, null, + null, null, null, null, null, null, null, + null + ), true, process.getProcess()); // This must succeed + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} 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 ee55ff66f..89fe3f83d 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java @@ -379,4 +379,58 @@ public void testThirdPartyUsersHaveTenantIds() throws Exception { user = ThirdParty.getUser(t1WithStorage.toAppIdentifierWithStorage(), signInUpResponse.user.id); assertArrayEquals(new String[]{"t2"}, user.tenantIds); } + + @Test + public void testThatDisassociateUserFromWrongTenantDoesNotWork() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user = TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t1"), "user@example.com", "password", process.getProcess()); + String userId = user.get("id").getAsString(); + + JsonObject response = TestMultitenancyAPIHelper.disassociateUserFromTenant(new TenantIdentifier(null, "a1", "t2"), userId, process.getProcess()); + assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + assertFalse(response.get("wasAssociated").getAsBoolean()); + } + + @Test + public void testThatDisassociateUserWithUseridMappingFromWrongTenantDoesNotWork() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user = TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t1"), "user@example.com", "password", process.getProcess()); + String userId = user.get("id").getAsString(); + + TestMultitenancyAPIHelper.createUserIdMapping(new TenantIdentifier(null, "a1", "t1"), userId, "externalid", process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.disassociateUserFromTenant(new TenantIdentifier(null, "a1", "t2"), "externalid", process.getProcess()); + assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + assertFalse(response.get("wasAssociated").getAsBoolean()); + } + + @Test + public void testAssociateAndDisassociateWithUseridMapping() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user = TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t1"), "user@example.com", "password", process.getProcess()); + String userId = user.get("id").getAsString(); + + TestMultitenancyAPIHelper.createUserIdMapping(new TenantIdentifier(null, "a1", "t1"), userId, "externalid", process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), "externalid", process.getProcess()); + assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + assertFalse(response.get("wasAlreadyAssociated").getAsBoolean()); + + response = TestMultitenancyAPIHelper.disassociateUserFromTenant(new TenantIdentifier(null, "a1", "t2"), "externalid", process.getProcess()); + assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); + assertTrue(response.get("wasAssociated").getAsBoolean()); + + } } diff --git a/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java index dbeca2544..90d9d3814 100644 --- a/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java @@ -29,6 +29,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.session.jwt.JWT; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -404,4 +405,23 @@ public void testRefreshSessionDoesNotWorkFromDifferentApp() throws Exception { session.get("refreshToken").getAsJsonObject().get("token").getAsString()); assertEquals("UNAUTHORISED", sessionResponse.get("status").getAsString()); } + + @Test + public void testAccessTokensContainsTid() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + JsonObject session = createSession(t1, "userid", new JsonObject(), new JsonObject()); + JWT.JWTInfo accessTokenInfo = JWT.getPayloadWithoutVerifying(session.get("accessToken").getAsJsonObject().get("token").getAsString()); + assertEquals(t1.getTenantId(), accessTokenInfo.payload.get("tId").getAsString()); + } + + { + JsonObject session = createSession(t2, "userid", new JsonObject(), new JsonObject()); + JWT.JWTInfo accessTokenInfo = JWT.getPayloadWithoutVerifying(session.get("accessToken").getAsJsonObject().get("token").getAsString()); + assertEquals(t2.getTenantId(), accessTokenInfo.payload.get("tId").getAsString()); + } + } }