From 586df49580dba5565e868964cf4044c3bf36a7d0 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 15 Sep 2023 17:34:20 +0530 Subject: [PATCH 1/4] fix: stats fix --- CHANGELOG.md | 4 +++ build.gradle | 2 +- .../java/io/supertokens/ee/EEFeatureFlag.java | 34 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) 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..17c5a513b 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -151,6 +151,26 @@ public void syncFeatureFlagWithLicenseKey() licenseKey = this.getLicenseKeyFromDb(); this.isLicenseKeyPresent = true; } catch (NoLicenseKeyFoundException ex) { + try { + // Need to check if multitenancy is enabled on the base app and then report paid usage stats + EE_FEATURES[] features = FeatureFlag.getInstance(main, new AppIdentifier(null, null)) + .getEnabledFeatures(); + for (EE_FEATURES feature : features) { + if (feature.equals(EE_FEATURES.MULTI_TENANCY)) { + licenseKey = this.getRootLicenseKeyFromDb(); + verifyLicenseKey(licenseKey); // also sends paid user stats for the app + try { + // small delay between license checks so that we have a delay for each license key check calls + Thread.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + break; + } + } + } catch (NoLicenseKeyFoundException ex2) { + // follow through below + } this.isLicenseKeyPresent = false; this.setEnabledEEFeaturesInDb(new EE_FEATURES[]{}); return; @@ -489,4 +509,18 @@ public String getLicenseKeyFromDb() Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), "Fetched license key from db: " + info.value); return info.value; } + + private String getRootLicenseKeyFromDb() + throws TenantOrAppNotFoundException, StorageQueryException, NoLicenseKeyFoundException { + Logging.debug(main, TenantIdentifier.BASE_TENANT, "Attempting to fetch license key from db"); + KeyValueInfo info = StorageLayer.getStorage(TenantIdentifier.BASE_TENANT, main) + .getKeyValue(TenantIdentifier.BASE_TENANT, 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"); + throw new NoLicenseKeyFoundException(); + } + Logging.debug(main, TenantIdentifier.BASE_TENANT, "Fetched license key from db: " + info.value); + return info.value; + + } } \ No newline at end of file From 42986e42fefaf4aeb7df6431eca977cb71a381a7 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 19 Sep 2023 11:51:35 +0530 Subject: [PATCH 2/4] fix: pr comments --- .../java/io/supertokens/ee/EEFeatureFlag.java | 59 ++++----- .../ee/test/TestMultitenancyStats.java | 124 ++++++++++++++++++ 2 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 17c5a513b..563841025 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 final List licenseCheckRequests = new ArrayList<>(); + private static final String[] ENTERPRISE_THIRD_PARTY_IDS = new String[] { "google-workspaces", "okta", @@ -152,22 +154,8 @@ public void syncFeatureFlagWithLicenseKey() this.isLicenseKeyPresent = true; } catch (NoLicenseKeyFoundException ex) { try { - // Need to check if multitenancy is enabled on the base app and then report paid usage stats - EE_FEATURES[] features = FeatureFlag.getInstance(main, new AppIdentifier(null, null)) - .getEnabledFeatures(); - for (EE_FEATURES feature : features) { - if (feature.equals(EE_FEATURES.MULTI_TENANCY)) { - licenseKey = this.getRootLicenseKeyFromDb(); - verifyLicenseKey(licenseKey); // also sends paid user stats for the app - try { - // small delay between license checks so that we have a delay for each license key check calls - Thread.sleep(5); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - break; - } - } + licenseKey = this.getRootLicenseKeyFromDb(); + verifyLicenseKey(licenseKey); // also sends paid user stats for the app } catch (NoLicenseKeyFoundException ex2) { // follow through below } @@ -414,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", @@ -496,31 +487,33 @@ 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 { - Logging.debug(main, TenantIdentifier.BASE_TENANT, "Attempting to fetch license key from db"); - KeyValueInfo info = StorageLayer.getStorage(TenantIdentifier.BASE_TENANT, main) - .getKeyValue(TenantIdentifier.BASE_TENANT, 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"); - throw new NoLicenseKeyFoundException(); - } - Logging.debug(main, TenantIdentifier.BASE_TENANT, "Fetched license key from db: " + info.value); - return info.value; + return getLicenseKeyInDb(TenantIdentifier.BASE_TENANT); + } + @TestOnly + public static List getLicenseCheckRequests() { + assert (Main.isTesting); + return licenseCheckRequests; } } \ 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..c5b5d7ca5 --- /dev/null +++ b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java @@ -0,0 +1,124 @@ +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)); + + { + // 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)); + } +} From 8d10ef78e2fbc9d262b53d51767d1a979a47dab9 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 19 Sep 2023 12:05:54 +0530 Subject: [PATCH 3/4] fix: disable for in mem --- .../java/io/supertokens/ee/test/TestMultitenancyStats.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java index c5b5d7ca5..d9a658e91 100644 --- a/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java +++ b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java @@ -46,6 +46,11 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception { 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(); From 9fae3d86cd7a2db64cf56346a9566e9f35435ee5 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 19 Sep 2023 12:24:30 +0530 Subject: [PATCH 4/4] fix: pr comments --- ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java | 11 +++++++++-- ee/src/test/java/io/supertokens/ee/test/Utils.java | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 563841025..56f02d526 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -59,7 +59,7 @@ 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 final List licenseCheckRequests = new ArrayList<>(); + private static List licenseCheckRequests = new ArrayList<>(); private static final String[] ENTERPRISE_THIRD_PARTY_IDS = new String[] { "google-workspaces", @@ -156,7 +156,7 @@ public void syncFeatureFlagWithLicenseKey() try { licenseKey = this.getRootLicenseKeyFromDb(); verifyLicenseKey(licenseKey); // also sends paid user stats for the app - } catch (NoLicenseKeyFoundException ex2) { + } catch (NoLicenseKeyFoundException | InvalidLicenseKeyException ex2) { // follow through below } this.isLicenseKeyPresent = false; @@ -516,4 +516,11 @@ 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/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 {