diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06af34a47..9c0aa2aa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -97,6 +97,10 @@ ALTER TABLE emailpassword_pswd_reset_tokens ADD CONSTRAINT emailpassword_pswd_re
ALTER TABLE emailpassword_pswd_reset_tokens ADD COLUMN email VARCHAR(256);
```
+## [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/README.md b/README.md
index bf258ea18..45d543f54 100644
--- a/README.md
+++ b/README.md
@@ -203,8 +203,13 @@ Mihály Lengyel
Nicholas Dudfield |
Qdea |
-
-Lukas Knuth |
+ Lukas Knuth |
+
+Melvyn Hills |
+
+ Matt Murray |
+Melvyn Hills
+
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 40eb83bb7..b68622959 100644
--- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
+++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
@@ -29,6 +29,7 @@
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
+import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
@@ -58,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",
@@ -150,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;
@@ -440,6 +449,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",
@@ -522,17 +534,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 {
diff --git a/jar/core-6.0.12.jar b/jar/core-6.0.13.jar
similarity index 55%
rename from jar/core-6.0.12.jar
rename to jar/core-6.0.13.jar
index 6e249ea87..c25370fb2 100644
Binary files a/jar/core-6.0.12.jar and b/jar/core-6.0.13.jar differ
diff --git a/src/test/java/io/supertokens/test/session/AccessTokenTest.java b/src/test/java/io/supertokens/test/session/AccessTokenTest.java
index 31e482a7a..30247227a 100644
--- a/src/test/java/io/supertokens/test/session/AccessTokenTest.java
+++ b/src/test/java/io/supertokens/test/session/AccessTokenTest.java
@@ -301,6 +301,7 @@ public void inputOutputTestStatic() throws Exception {
TokenInfo newToken = AccessToken.createNewAccessToken(process.getProcess(), "sessionHandle", "userId",
"refreshTokenHash1", "parentRefreshTokenHash1", jsonObj, "antiCsrfToken", expiryTime,
AccessToken.getLatestVersion(), true);
+ System.out.println(newToken.token);
AccessTokenInfo info = AccessToken.getInfoFromAccessToken(process.getProcess(), newToken.token, true);
assertEquals("sessionHandle", info.sessionHandle);
assertEquals("userId", info.recipeUserId);