diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 81c80b4ea..79af8d666 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -185,37 +185,6 @@ private JsonObject getDashboardLoginStats() throws TenantOrAppNotFoundException, return stats; } - private JsonObject getTOTPStats() throws StorageQueryException, TenantOrAppNotFoundException { - JsonObject totpStats = new JsonObject(); - JsonArray totpMauArr = new JsonArray(); - - Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); - - // TODO Active users are present only on public tenant and TOTP users may be present on different storages - Storage publicTenantStorage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main); - final long now = System.currentTimeMillis(); - for (int i = 0; i < 30; i++) { - long today = now - (now % (24 * 60 * 60 * 1000L)); - long timestamp = today - (i * 24 * 60 * 60 * 1000L); - - int totpMau = 0; - // TODO Need to figure out a way to combine the data from different storages to get the final stats - // for (Storage storage : storages) { - totpMau += ((ActiveUsersStorage) publicTenantStorage).countUsersEnabledTotpAndActiveSince(this.appIdentifier, timestamp); - // } - totpMauArr.add(new JsonPrimitive(totpMau)); - } - - totpStats.add("maus", totpMauArr); - - int totpTotalUsers = 0; - for (Storage storage : storages) { - totpTotalUsers += ((ActiveUsersStorage) storage).countUsersEnabledTotp(this.appIdentifier); - } - totpStats.addProperty("total_users", totpTotalUsers); - return totpStats; - } - private boolean isEnterpriseThirdPartyId(String thirdPartyId) { for (String enterpriseThirdPartyId : ENTERPRISE_THIRD_PARTY_IDS) { if (thirdPartyId.startsWith(enterpriseThirdPartyId)) { @@ -225,37 +194,29 @@ private boolean isEnterpriseThirdPartyId(String thirdPartyId) { return false; } - private JsonObject getMFAStats() throws StorageQueryException, TenantOrAppNotFoundException{ - JsonObject mfaStats = new JsonObject(); - JsonArray mfaMauArr = new JsonArray(); - - Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); - // TODO: Active users are present only on public tenant and MFA users may be present on different storages - Storage publicTenantStorage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main); - final long now = System.currentTimeMillis(); - for (int i = 0; i < 30; i++) { - long today = now - (now % (24 * 60 * 60 * 1000L)); - long timestamp = today - (i * 24 * 60 * 60 * 1000L); + JsonObject result = new JsonObject(); + Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); - int mfaMau = 0; - // TODO Need to figure out a way to combine the data from different storages to get the final stats - // for (Storage storage : storages) { - mfaMau += ((ActiveUsersStorage) publicTenantStorage).countUsersEnabledMfaAndActiveSince(this.appIdentifier, timestamp); - // } - mfaMauArr.add(new JsonPrimitive(mfaMau)); - } + int totalUserCountWithMoreThanOneLoginMethod = 0; + int[] maus = new int[30]; - mfaStats.add("maus", mfaMauArr); - mfaStats.add("totp", getTOTPStats()); + long now = System.currentTimeMillis(); + long today = now - (now % (24 * 60 * 60 * 1000L)); - int mfaTotalUsers = 0; for (Storage storage : storages) { - mfaTotalUsers += ((ActiveUsersStorage) storage).countUsersEnabledMfa(this.appIdentifier); + totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this.appIdentifier); + + for (int i = 0; i < 30; i++) { + long timestamp = today - (i * 24 * 60 * 60 * 1000L); + maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(appIdentifier, timestamp); + } } - mfaStats.addProperty("total_users", mfaTotalUsers); - return mfaStats; + + result.addProperty("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled", totalUserCountWithMoreThanOneLoginMethod); + result.add("mauWithMoreThanOneLoginMethodOrTOTPEnabled", new Gson().toJsonTree(maus)); + return result; } private JsonObject getMultiTenancyStats() @@ -306,6 +267,7 @@ private JsonObject getMultiTenancyStats() } private JsonObject getAccountLinkingStats() throws StorageQueryException { + // TODO: Active users are present only on public tenant and MFA users may be present on different storages JsonObject result = new JsonObject(); Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); boolean usesAccountLinking = false; diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 19fce809c..09ad8ffdc 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -1211,44 +1211,6 @@ public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws } } - @Override - public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) - throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int countUsersEnabledMfa(AppIdentifier appIdentifier) throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledMfa(this, appIdentifier); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int countUsersEnabledMfaAndActiveSince(AppIdentifier appIdentifier, long time) - throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledMfaAndActiveSince(this, appIdentifier, time); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { @@ -2997,4 +2959,13 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A } } + @Override + public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier appIdentifier) throws StorageQueryException { + return 0; // TODO + } + + @Override + public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, long timestamp) throws StorageQueryException { + return 0; // TODO + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java index 81b14e7f5..43be2639b 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java @@ -41,6 +41,7 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { + // TODO: Active users are present only on public tenant and MFA users may be present on different storages String QUERY = "SELECT count(1) as c FROM (" + " SELECT count(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + " FROM " + Config.getConfig(start).getUsersTable() @@ -61,48 +62,6 @@ public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, }); } - public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier) - throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() - + " WHERE app_id = ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - }, result -> { - if (result.next()) { - return result.getInt("total"); - } - return 0; - }); - } - - public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) - throws SQLException, StorageQueryException { - String QUERY = - "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " AS totp_users " - + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " - + "ON totp_users.user_id = user_last_active.user_id " - + "WHERE user_last_active.app_id = ? AND user_last_active.last_active_time >= ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setLong(2, sinceTime); - }, result -> { - if (result.next()) { - return result.getInt("total"); - } - return 0; - }); - } - - public static int countUsersEnabledMfa(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - return 0; // TODO - } - - public static int countUsersEnabledMfaAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { - return 0; // TODO - } - public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index ea3ac89f4..eca26362d 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -29,6 +29,7 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; @@ -340,6 +341,16 @@ protected AppIdentifierWithStorage getAppIdentifierWithStorageFromRequestAndEnfo storage, storages); } + protected AppIdentifierWithStorage getPublicTenantStorage(HttpServletRequest req) + throws ServletException, TenantOrAppNotFoundException { + AppIdentifier appIdentifier = new AppIdentifier(this.getConnectionUriDomain(req), this.getAppId(req)); + + Storage storage = StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main); + + return appIdentifier.withStorage(storage); + + } + protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest( HttpServletRequest req, String userId, UserIdType userIdType) throws StorageQueryException, TenantOrAppNotFoundException, UnknownUserIdException, ServletException { diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java index 5695188da..4074b3ab8 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java @@ -78,7 +78,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I password); io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); - ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.getSupertokensUserId()); // use the internal user id JsonObject result = new JsonObject(); diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java index c99791fea..80deeb863 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java @@ -81,7 +81,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req); AuthRecipeUserInfo user = EmailPassword.signUp(tenant, super.main, normalisedEmail, password); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java index c34aa2fc3..48fbec52b 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -90,7 +90,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)); io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); diff --git a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java index ee92da0bd..1749e154b 100644 --- a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java @@ -90,10 +90,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), sessionInfo.session.userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(appIdentifierWithStorage, main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(appIdentifierWithStorage, main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, sessionInfo.session.userId); } } catch (StorageQueryException ignored) { diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java index 4263285c2..7691ee81e 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java @@ -110,10 +110,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), sessionInfo.session.userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, sessionInfo.session.userId); } } catch (StorageQueryException ignored) { diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java index 127e0b5fc..22fba74cf 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java @@ -116,10 +116,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, userId); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userId); } } catch (StorageQueryException ignored) { } diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java index ad626a64e..caf319f63 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -81,7 +81,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I thirdPartyUserId, email, isEmailVerified); UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -140,7 +140,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I email, isEmailVerified); UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); diff --git a/src/test/java/io/supertokens/test/ActiveUsersTest.java b/src/test/java/io/supertokens/test/ActiveUsersTest.java index c88a5bbef..108bc38ee 100644 --- a/src/test/java/io/supertokens/test/ActiveUsersTest.java +++ b/src/test/java/io/supertokens/test/ActiveUsersTest.java @@ -4,10 +4,15 @@ import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper; +import io.supertokens.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -212,4 +217,80 @@ public void activeUserCountAPITest() throws Exception { assert res.get("count").getAsInt() == 2; } + @Test + public void testThatActiveUserDataIsSavedInPublicTenantStorage() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + 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; + } + + { // Create a tenant + JsonObject coreConfig = new JsonObject(); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", true, true, true, + coreConfig); + } + + { // no active users yet + HashMap params = new HashMap<>(); + params.put("since", "0"); + JsonObject res = HttpRequestForTesting.sendGETRequest( + process.getProcess(), + "", + "http://localhost:3567/users/count/active", + params, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + ""); + + assert res.get("status").getAsString().equals("OK"); + assert res.get("count").getAsInt() == 0; + } + + { // Sign up, which updates active users + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/t1/recipe/signup", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + } + + { // 1 active user in the public tenant + HashMap params = new HashMap<>(); + params.put("since", "0"); + JsonObject res = HttpRequestForTesting.sendGETRequest( + process.getProcess(), + "", + "http://localhost:3567/users/count/active", + params, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + ""); + + assert res.get("status").getAsString().equals("OK"); + assert res.get("count").getAsInt() == 1; + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java index 5da1a28cc..f03c7310e 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; @@ -28,7 +29,9 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -153,8 +156,11 @@ public void testThatCallingGetFeatureFlagAPIReturnsEmptyArray() throws Exception Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + private final String OPAQUE_KEY_WITH_MFA_MULTITENANCY_FEATURE = "wtdfQK80jaEYmM1cqlW=lELizFWJlaHOggzvF59jOAwX7NFx" + + "dxH1fw0=RTy=BZixibzF5rn85SNKwfFfLcMm6Li3l1DYOVVD3H8XymCcekti217BxXb-Q6y5r-SKwMOG"; + @Test - public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception { + public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -164,7 +170,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception return; } - FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_MFA_MULTITENANCY_FEATURE); // Get the stats without any users/activity { @@ -180,22 +186,22 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception if (StorageLayer.isInMemDb(process.main)) { assert features.size() == EE_FEATURES.values().length; } else { - assert features.size() == 1; + assert features.size() == 2; // MFA + MULTITENANCY } assert features.contains(new JsonPrimitive("mfa")); assert maus.size() == 30; assert maus.get(0).getAsInt() == 0; assert maus.get(29).getAsInt() == 0; - JsonObject totpStats = usageStats.get("mfa").getAsJsonObject().get("totp").getAsJsonObject(); - JsonArray totpMaus = totpStats.get("maus").getAsJsonArray(); - int totalTotpUsers = totpStats.get("total_users").getAsInt(); + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); - assert totpMaus.size() == 30; - assert totpMaus.get(0).getAsInt() == 0; - assert totpMaus.get(29).getAsInt() == 0; + assert mfaMaus.size() == 30; + assert mfaMaus.get(0).getAsInt() == 0; + assert mfaMaus.get(29).getAsInt() == 0; - assert totalTotpUsers == 0; + assert totalMfaUsers == 0; } // First register 2 users for emailpassword recipe. @@ -206,23 +212,25 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception JsonObject signUpResponse2 = Utils.signUpRequest_2_5(process, "random2@gmail.com", "validPass123"); assert signUpResponse2.get("status").getAsString().equals("OK"); - // Now enable TOTP for the first user by registering a device. - JsonObject body = new JsonObject(); - body.addProperty("userId", signUpResponse.get("user").getAsJsonObject().get("id").getAsString()); - body.addProperty("deviceName", "d1"); - body.addProperty("skew", 0); - body.addProperty("period", 30); - JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( - process.getProcess(), - "", - "http://localhost:3567/recipe/totp/device", - body, - 1000, - 1000, - null, - Utils.getCdiVersionStringLatestForTests(), - "totp"); - assert res.get("status").getAsString().equals("OK"); + { + // Now enable TOTP for the first user by registering a device. + JsonObject body = new JsonObject(); + body.addProperty("userId", signUpResponse.get("user").getAsJsonObject().get("id").getAsString()); + body.addProperty("deviceName", "d1"); + body.addProperty("skew", 0); + body.addProperty("period", 30); + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + } // Now check the stats again: { @@ -238,7 +246,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception if (StorageLayer.isInMemDb(process.main)) { assert features.size() == EE_FEATURES.values().length; } else { - assert features.size() == 1; + assert features.size() == 2; // MFA + MULTITENANCY } assert features.contains(new JsonPrimitive("mfa")); @@ -246,15 +254,138 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception assert maus.get(0).getAsInt() == 2; // 2 users have signed up assert maus.get(29).getAsInt() == 2; - JsonObject totpStats = usageStats.get("mfa").getAsJsonObject().get("totp").getAsJsonObject(); - JsonArray totpMaus = totpStats.get("maus").getAsJsonArray(); - int totalTotpUsers = totpStats.get("total_users").getAsInt(); + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); - assert totpMaus.size() == 30; - assert totpMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled - assert totpMaus.get(29).getAsInt() == 1; + assert mfaMaus.size() == 30; + assert mfaMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled + assert mfaMaus.get(29).getAsInt() == 1; + + assert totalMfaUsers == 1; + } + + { + // Test with account linking + JsonObject user1 = Utils.signUpRequest_2_5(process, "test1@gmail.com", "validPass123"); + assert signUpResponse.get("status").getAsString().equals("OK"); - assert totalTotpUsers == 1; + JsonObject user2 = Utils.signUpRequest_2_5(process, "test2@gmail.com", "validPass123"); + assert signUpResponse2.get("status").getAsString().equals("OK"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.get("user").getAsJsonObject().get("id").getAsString(), true); + AuthRecipe.linkAccounts(process.getProcess(), user2.get("user").getAsJsonObject().get("id").getAsString(), user1.get("user").getAsJsonObject().get("id").getAsString(), true); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); + + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + if (StorageLayer.isInMemDb(process.main)) { + assert features.size() == EE_FEATURES.values().length; + } else { + assert features.size() == 2; // MFA + MULTITENANCY + } + + assert features.contains(new JsonPrimitive("mfa")); + assert maus.size() == 30; + assert maus.get(0).getAsInt() == 4; // 2 users have signed up + assert maus.get(29).getAsInt() == 4; + + { + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 30; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + assert totalMfaUsers == 2; + } + + // Add TOTP to the linked user + { + JsonObject body = new JsonObject(); + body.addProperty("userId", user1.get("user").getAsJsonObject().get("id").getAsString()); + body.addProperty("deviceName", "d1"); + body.addProperty("skew", 0); + body.addProperty("period", 30); + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + } + } + + { + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); + + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + { // MFA stats should still count 2 users + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 30; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + assert totalMfaUsers == 2; + } + } + + { // Associate the user with multiple tenants and still the stats should be same + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + new TotpConfig(false), null, null, + new JsonObject() + ), false); + Multitenancy.addUserIdToTenant( + process.getProcess(), + new TenantIdentifier(null, null, "t1").withStorage(StorageLayer.getStorage(process.getProcess())), + signUpResponse.get("user").getAsJsonObject().get("id").getAsString() + ); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); + + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + { // MFA stats should still count 2 users + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 30; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + assert totalMfaUsers == 2; + } } process.kill();