diff --git a/CHANGELOG.md b/CHANGELOG.md index a858ddb5a..ab165dc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +## [6.0.13] - 2023-09-15 + +- Fixes paid stats reporting for multitenancy + ## [6.0.12] - 2023-09-04 - Fixes randomly occurring `serialization error for concurrent update` in `verifySession` API diff --git a/build.gradle b/build.gradle index 9d329dff7..5ae32374e 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "6.0.12" +version = "6.0.13" repositories { diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 8efe42552..56f02d526 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -59,6 +59,8 @@ public class EEFeatureFlag implements io.supertokens.featureflag.EEFeatureFlagIn public static final String FEATURE_FLAG_KEY_IN_DB = "FEATURE_FLAG"; public static final String LICENSE_KEY_IN_DB = "LICENSE_KEY"; + private static List licenseCheckRequests = new ArrayList<>(); + private static final String[] ENTERPRISE_THIRD_PARTY_IDS = new String[] { "google-workspaces", "okta", @@ -151,6 +153,12 @@ public void syncFeatureFlagWithLicenseKey() licenseKey = this.getLicenseKeyFromDb(); this.isLicenseKeyPresent = true; } catch (NoLicenseKeyFoundException ex) { + try { + licenseKey = this.getRootLicenseKeyFromDb(); + verifyLicenseKey(licenseKey); // also sends paid user stats for the app + } catch (NoLicenseKeyFoundException | InvalidLicenseKeyException ex2) { + // follow through below + } this.isLicenseKeyPresent = false; this.setEnabledEEFeaturesInDb(new EE_FEATURES[]{}); return; @@ -394,6 +402,9 @@ private EE_FEATURES[] doServerCall(String licenseKey) json.addProperty("licenseKey", licenseKey); json.addProperty("superTokensVersion", Version.getVersion(main).getCoreVersion()); json.add("paidFeatureUsageStats", this.getPaidFeatureStats()); + if (Main.isTesting) { + licenseCheckRequests.add(json); + } ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.LICENSE_KEY_CHECK_NETWORK_CALL, null); JsonObject licenseCheckResponse = HttpRequest.sendJsonPOSTRequest(this.main, REQUEST_ID, "https://api.supertokens.io/0/st/license/check", @@ -476,17 +487,40 @@ private void removeLicenseKeyFromDb() throws StorageQueryException, TenantOrAppN new KeyValueInfo(LICENSE_KEY_IN_DB_NOT_PRESENT_VALUE)); } - @Override - public String getLicenseKeyFromDb() - throws NoLicenseKeyFoundException, StorageQueryException, TenantOrAppNotFoundException { - Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "Attempting to fetch license key from db"); - KeyValueInfo info = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main) - .getKeyValue(this.appIdentifier.getAsPublicTenantIdentifier(), LICENSE_KEY_IN_DB); + private String getLicenseKeyInDb(TenantIdentifier tenantIdentifier) + throws TenantOrAppNotFoundException, StorageQueryException, NoLicenseKeyFoundException { + Logging.debug(main, tenantIdentifier, "Attempting to fetch license key from db"); + KeyValueInfo info = StorageLayer.getStorage(tenantIdentifier, main) + .getKeyValue(tenantIdentifier, LICENSE_KEY_IN_DB); if (info == null || info.value.equals(LICENSE_KEY_IN_DB_NOT_PRESENT_VALUE)) { - Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "No license key found in db"); + Logging.debug(main, tenantIdentifier, "No license key found in db"); throw new NoLicenseKeyFoundException(); } - Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "Fetched license key from db: " + info.value); + Logging.debug(main, tenantIdentifier, "Fetched license key from db: " + info.value); return info.value; } + + @Override + public String getLicenseKeyFromDb() + throws NoLicenseKeyFoundException, StorageQueryException, TenantOrAppNotFoundException { + return getLicenseKeyInDb(appIdentifier.getAsPublicTenantIdentifier()); + } + + private String getRootLicenseKeyFromDb() + throws TenantOrAppNotFoundException, StorageQueryException, NoLicenseKeyFoundException { + return getLicenseKeyInDb(TenantIdentifier.BASE_TENANT); + } + + @TestOnly + public static List getLicenseCheckRequests() { + assert (Main.isTesting); + return licenseCheckRequests; + } + + @TestOnly + public static void resetLisenseCheckRequests() { + licenseCheckRequests = new ArrayList<>(); + } + + } \ No newline at end of file diff --git a/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java new file mode 100644 index 000000000..d9a658e91 --- /dev/null +++ b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java @@ -0,0 +1,129 @@ +package io.supertokens.ee.test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.ee.EEFeatureFlag; +import io.supertokens.ee.cronjobs.EELicenseCheck; +import io.supertokens.ee.test.httpRequest.HttpRequestForTesting; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.webserver.WebserverAPI; +import org.junit.*; +import org.junit.rules.TestRule; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class TestMultitenancyStats { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + FeatureFlag.clearURLClassLoader(); + } + + private final String OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-" + + "BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; + + @Test + public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception { + String[] args = {"../../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + CronTaskTest.getInstance(process.main).setIntervalInSeconds(EELicenseCheck.RESOURCE_KEY, 1); + Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.isInMemDb(process.main)) { + // cause we keep all features enabled in memdb anyway + return; + } + + { + // Add the license + JsonObject requestBody = new JsonObject(); + + requestBody.addProperty("licenseKey", OPAQUE_KEY_WITH_MULTITENANCY_FEATURE); + + HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", + "http://localhost:3567/ee/license", + requestBody, 10000, 10000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + } + + { + // Create tenants and apps + JsonObject config = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier("127.0.0.1", null, null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier("127.0.0.1", "a1", null), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier("127.0.0.1", "a1", "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ), false); + } + + Thread.sleep(2000); // Let all the cron tasks run + + List requests = EEFeatureFlag.getLicenseCheckRequests(); + Set tenantIdentifiers = new HashSet<>(); + + for (JsonObject request : requests) { + if (request.has("paidFeatureUsageStats")) { + JsonObject paidStats = request.getAsJsonObject("paidFeatureUsageStats"); + if (paidStats.has("multi_tenancy")) { + JsonObject mtStats = paidStats.getAsJsonObject("multi_tenancy"); + String cud = mtStats.get("connectionUriDomain").getAsString(); + String appId = mtStats.get("appId").getAsString(); + + JsonArray tenants = mtStats.get("tenants").getAsJsonArray(); + for (JsonElement tenantElem : tenants) { + JsonObject tenant = tenantElem.getAsJsonObject(); + String tenantId = tenant.get("tenantId").getAsString(); + + tenantIdentifiers.add(new TenantIdentifier(cud, appId, tenantId)); + } + } + } + } + + Assert.assertEquals(tenantIdentifiers.size(), 4); + Assert.assertTrue(tenantIdentifiers.contains(new TenantIdentifier(null, null, null))); + Assert.assertTrue(tenantIdentifiers.contains(new TenantIdentifier("127.0.0.1", null, null))); + Assert.assertTrue(tenantIdentifiers.contains(new TenantIdentifier("127.0.0.1", "a1", null))); + Assert.assertTrue(tenantIdentifiers.contains(new TenantIdentifier("127.0.0.1", "a1", "t1"))); + + process.kill(); + Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/ee/src/test/java/io/supertokens/ee/test/Utils.java b/ee/src/test/java/io/supertokens/ee/test/Utils.java index ae4223ec5..2235c3349 100644 --- a/ee/src/test/java/io/supertokens/ee/test/Utils.java +++ b/ee/src/test/java/io/supertokens/ee/test/Utils.java @@ -1,6 +1,7 @@ package io.supertokens.ee.test; import io.supertokens.Main; +import io.supertokens.ee.EEFeatureFlag; import io.supertokens.pluginInterface.PluginInterfaceTesting; import io.supertokens.storageLayer.StorageLayer; import org.apache.tomcat.util.http.fileupload.FileUtils; @@ -51,6 +52,7 @@ public static void reset() { Main.isTesting = true; PluginInterfaceTesting.isTesting = true; Main.makeConsolePrintSilent = true; + EEFeatureFlag.resetLisenseCheckRequests(); String installDir = "../../"; try {